前言介绍

  • JavaAgent是在JDK5之后提供的新特性,又叫叫java代理。开发人员可通过这种机制(Instrumentation)在jvm加载class文件之前修改类的字节码,动态更改类方法实现AOP,提供监控服务如:方法调用时长、jvm内存等。
  • 修改字节码领域有三个比较常见的框架;ASM、byte-buddy、javassist,其操作方式和控制粒度不同。
  • ASM 更偏向于底层,直接面向字节码编程,需要了解 JVM 虚拟机中指定规范以及对局部变量以及操作数栈的知识。虽然在编写起来比较麻烦,但是它也是性能最好功能最强的字节码操作库。 CGLIB 动态代理使用的就是ASM。
  • Javassist与byte-buddy提供了强大的 API,操作使用上更加容易控制,可以在不了解Java字节码规范的前提下修改class文件。

案例

使用javassist统计方法执行耗时

  • 引入maven依赖
<dependency>
   <groupId>org.javassist</groupId>
   <artifactId>javassist</artifactId>
   <version>3.25.0-GA</version>
</dependency>
  • 将javassist打包到构建的javaAgent包中
<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <compilerArgument>-parameters</compilerArgument>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
            <!-- 将javassist包打包到Agent中 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <artifactSet>
                        <includes>
                            <include>org.javassist:javassist:jar:</include>
                        </includes>
                    </artifactSet>
                </configuration>
            </plugin>
        </plugins>
    </build>
  • 编写耗时统计,在目标方法字节码前后插入方法耗时统计逻辑
public class ClazzTransform implements ClassFileTransformer {

    /** 增强类所在包名白名单 */
    private final String BASE_PACKAGE;

    public ClazzTransform(String basePackage) {
        this.BASE_PACKAGE = basePackage;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
        className = className.replace("/", ".");
        if ( !className.startsWith(BASE_PACKAGE) ){
            return null;
        }
        try {
            CtClass ctKlass = ClassPool.getDefault().get(className);
            CtBehavior[] behaviors = ctKlass.getDeclaredBehaviors();
            //遍历方法进行增强
            for (CtBehavior m : behaviors) {
                enhanceMethod(m);
            }
            byte[] bytes = ctKlass.toBytecode();
            //输出修改后的class文件内容
            Files.write(Paths.get("D:\\A.class"), bytes);
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classFileBuffer;
    }
    /** 方法增强,添加方法耗时统计 */
    private void enhanceMethod(CtBehavior method) throws CannotCompileException {
        if ( method.isEmpty() ){
            return;
        }
        String methodName = method.getName();
        method.addLocalVariable("start", CtClass.longType);
        method.insertBefore("start = System.currentTimeMillis();");
        method.insertAfter( String.format("System.out.println(\"%s cost: \" + (System.currentTimeMillis() - start) + \"ms\");", methodName) );
    }
}
  • 编写探针引导类
    类似于java主类的main方法,jvm会调用java agent的premain方法
public class AgentPremain {

    public static void premain(String ages, Instrumentation instrumentation){
        instrumentation.addTransformer( new ClazzTransform("com.lauor.agent") );
    }
    //如果没有实现上面的方法,JVM将尝试调用该方法
    public static void premain(String agentArgs) {
    }
}
  • 配置jar包探针引导类

java监控线程状态 java监控方法耗时_java监控线程状态

Manifest-Version: 1.0
Premain-Class: com.lauor.agent.AgentPremain
Can-Redefine-Classes: true
  • 测试
  • 基于jdk11 httpclient调用百度首页
public class AgentTest {

    public static void main(String[] args) throws Exception {
        timeMonitor();
    }

    private static void timeMonitor() throws Exception {
        HttpRequest request = HttpRequest.newBuilder( URI.create("https://www.baidu.com/") ).GET().build();
        HttpResponse<String> rs = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(rs);
    }
}
  • 配置带有javaagent的启动参数
-javaagent:D:\agent\target\agent-1.1.1-SNAPSHOT.jar=agentArgs

java监控线程状态 java监控方法耗时_prometheus_02

  • 调用结果

java监控线程状态 java监控方法耗时_maven_03

  • 查看修改后的class文件

java监控线程状态 java监控方法耗时_maven_04

使用bytebuddy统计方法调用耗时

  • 介绍
    使用bytebuddy比使用javassist更为简单,使用上类似于Java动态代理模式
  • 引入maven依赖
<properties>
    <bytebuddy.version>1.11.22</bytebuddy.version>
</properties>
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>${bytebuddy.version}</version>
</dependency>
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>${bytebuddy.version}</version>
    <scope>test</scope>
</dependency>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven.compiler.version}</version>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <compilerArgument>-parameters</compilerArgument>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                </archive>
            </configuration>
        </plugin>
        <!-- 将bytebuddy包打包到Agent中 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <artifactSet>
                    <includes>
                        <include>net.bytebuddy:byte-buddy:jar:</include>
                        <include>net.bytebuddy:byte-buddy-agent:jar:</include>
                    </includes>
                </artifactSet>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 编写耗时统计逻辑
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class MethodMonitor {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        long sTime = System.currentTimeMillis();
        try {
            return callable.call();
        } finally {
            System.out.println( String.format("%s cost %dms", method.getName(), System.currentTimeMillis() - sTime) );
        }
    }
}
  • 编写探针引导类
public static void premain(String ages, Instrumentation instrumentation){
    System.out.println( String.format("invoke premain args=%s", ages) );

    runMethodCost(instrumentation);
}
private static void runMethodCost(Instrumentation instrumentation) {
   AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> builder
       //拦截任意方法
       .method(ElementMatchers.any())
       //委托
       .intercept(MethodDelegation.to(MethodMonitor.class));

   new AgentBuilder
       .Default()
       //指定需要拦截的类
       .type(ElementMatchers.nameStartsWith("com.lauor.agent"))
       .transform(transformer)
       .installOn(instrumentation);
}
  • 同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数
  • 运行javassist的基于jdk11 httpclient调用百度首页的例子

java监控线程状态 java监控方法耗时_java监控线程状态_05

使用javaagent监控jvm

  • 编写代码收集jvm gc与内存信息
public class JvmMonitor {

    /** 堆内存手机 */
    public void gatherHeap(){
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapMemory = memoryMXBean.getHeapMemoryUsage();

        //初始堆内存
        BigDecimal initSize = toMb( heapMemory.getInit() );
        BigDecimal usedSize = toMb( heapMemory.getUsed() );
        BigDecimal maxSize = toMb( heapMemory.getMax() );
        //os已分配大小
        BigDecimal committedSize = toMb( heapMemory.getCommitted() );

        String info = String.format("init:%sMB,max:%sMB,committed:%sMB,used:%sMB",
            initSize.floatValue(), maxSize.floatValue(), committedSize.floatValue(), usedSize.floatValue());

        System.out.println(info);
    }

    /** 字节转mb */
    private BigDecimal toMb(long bytes){
        final long mInBytes = 1024 * 1024;
        //四舍五入保留一位小数
        BigDecimal value = BigDecimal.valueOf(bytes).divide(BigDecimal.valueOf(mInBytes), 1, RoundingMode.HALF_UP);
        return value;
    }

    /** GC信息收集 */
    public void gatherGc(){
        List<GarbageCollectorMXBean> gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean gcMXBean : gcMXBeans) {
            //gc总计次数
            long count = gcMXBean.getCollectionCount();
            //gc名
            String name = gcMXBean.getName();
            //gc大致耗时
            long costInMill = gcMXBean.getCollectionTime();
            String memNames = Arrays.deepToString( gcMXBean.getMemoryPoolNames() );

            String info = String.format("name:%s,count:%s,cost %dms,pool name:%s",
                name, count, costInMill, memNames);
            System.out.println(info);
        }
    }
}
  • 编写定时任务,定时执行收集jvm gc与内存信息任务
public class AgentPremain {

    public static void premain(String ages, Instrumentation instrumentation){
        runJvmTask();
    }

    private static void runJvmTask(){
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "pool-jvm-" + threadNumber.getAndIncrement());
                if (t.isDaemon()){
                    t.setDaemon(false);
                }
                if (t.getPriority() != Thread.NORM_PRIORITY){
                    t.setPriority(Thread.NORM_PRIORITY);
                }
                return t;
            }
        });

        JvmMonitor jvmMonitor = new JvmMonitor();
        executorService.scheduleAtFixedRate(() -> {
            jvmMonitor.gatherGc();
            jvmMonitor.gatherHeap();
        }, 5, 5, TimeUnit.SECONDS);
    }
}
  • 同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数
  • 编写样例不停创建对象,模拟线上后端应用
public class AgentTest {

    public static void main(String[] args) throws Exception {
        jvmMonitor();
    }

    private static void jvmMonitor(){
        while (true){
            List<String> list = new ArrayList<>();
            list.add("print jvm heap");
            list.add("print jvm gc");
            list.clear();
        }
    }
}
  • 运行结果

java监控线程状态 java监控方法耗时_prometheus_06

  • 开箱即用的基于javaagent的监控解决方案:prometheus(普罗米修斯)
  • 介绍:
    prometheus是一个开源的监控系统和报警工具集合。主要有以下特点:
    1:由指标名称和和键/值对标签标识的时间序列数据组成的多维数据模型。
    2:强大的查询语言 PromQL。
    3:不依赖分布式存储;单个服务节点具有自治能力。
    4:时间序列数据是服务端通过 HTTP 协议主动拉取获得的,
    也可以通过中间网关来推送时间序列数据。
    5:可以通过静态配置文件或服务发现来获取监控目标。
    6:支持多种类型的图表和仪表盘。
  • prometheus结合grafana的效果图

java监控线程状态 java监控方法耗时_prometheus_07