手把手教你实现一个方法耗时统计的 java agent

简介: 本篇博文将进入 java agent 的实战,手把手教你如何是实现一个统计方法耗时的 java agent

image.png


前面有两篇铺垫博文,在博文《200303-如何优雅的在 java 中统计代码块耗时》,其最后提到了根据利用 java agent 来统计方法耗时


博文《200316-IDEA + maven 零基础构建 java agent 项目》中则详细描述了搭建一个 java agent 开发测试项目的全过程


本篇博文将进入 java agent 的实战,手把手教你如何是实现一个统计方法耗时的 java agent


1. 基本姿势点



上面两节虽然手把手教你实现了一个 hello world 版 agent,然而实际上对 java agent 依然是一脸茫然,所以我们得先补齐一下基础知识


首先来看 agent 的两个方法中的参数 Instrumentation,我们先看一下它的接口定义

/**
 * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
 * Transformer可以直接对类的字节码byte[]进行修改
 */
void addTransformer(ClassFileTransformer transformer);
/**
 * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
 * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
 * 获取一个对象的大小
 */
long getObjectSize(Object objectToSize);
/**
 * 将一个jar加入到bootstrap classloader的 classpath里
 */
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
 * 获取当前被JVM加载的所有类对象
 */
Class[] getAllLoadedClasses();
复制代码


前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。


通过上面的描述,可知


  • 可以通过Transformer修改类
  • 类加载时,会被触发 Transformer 拦截


2. 实现


我们需要统计方法耗时,所以想到的就是在方法的执行前,记录一个时间,执行完之后统计一下时间差,即为耗时


直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码

实现自定义的ClassFileTransformer,代码如下


public class CostTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        // 这里我们限制下,只针对目标包下进行耗时统计
        if (!className.startsWith("com/git/hui/java/")) {
            return classfileBuffer;
        }
        CtClass cl = null;
        try {
            ClassPool classPool = ClassPool.getDefault();
            cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
            for (CtMethod method : cl.getDeclaredMethods()) {
                // 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
                method.addLocalVariable("start", CtClass.longType);
                method.insertBefore("start = System.currentTimeMillis();");
                String methodName = method.getLongName();
                method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +
                        ".currentTimeMillis() - start));");
            }
            byte[] transformed = cl.toBytecode();
            return transformed;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}
复制代码


然后稍微改一下 agent


/**
 * Created by @author yihui in 16:39 20/3/15.
 */
public class SimpleAgent {
    /**
     * jvm 参数形式启动,运行此方法
     *
     * manifest需要配置属性Premain-Class
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
        customLogic(inst);
    }
    /**
     * 动态 attach 方式启动,运行此方法
     *
     * manifest需要配置属性Agent-Class
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
        customLogic(inst);
    }
    /**
     * 统计方法耗时
     *
     * @param inst
     */
    private static void customLogic(Instrumentation inst) {
        inst.addTransformer(new CostTransformer(), true);
    }
}
复制代码


到此 agent 完毕,打包和上面的过程一样,接下来进入测试环节


创建一个 DemoClz, 里面两个方法


public class DemoClz {
    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }
    public int count(int i) {
        System.out.println("cnt: " + i);
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 1;
    }
}
复制代码


然后对应的 main 方法如下

public class BaseMain {
    public static void main(String[] args) throws InterruptedException {
        DemoClz demoClz = new DemoClz();
        int cnt = 0;
        for (int i = 0; i < 20; i++) {
            if (++cnt % 2 == 0) {
                i = demoClz.print(i);
            } else {
                i = demoClz.count(i);
            }
        }
    }
}
复制代码


选择 jvm 参数指定 agent 方式运行(具体操作和上面一样),输出如下


image.png


虽然我们的应用程序中并没有方法的耗时统计,但是最终的输出却完美的打印了每个方法的调用耗时,实现了无侵入的耗时统计功能


到这里本文的 java agent 的扫盲 + 实战(开发一个方法耗时统计)都已经完成了,是否就宣告着可以小结了,并不是,下面介绍一下在实现上面的 demo 过程中遇到的一个问题


3. Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame


在演示方法耗时的 agent 的示例中,并没有借助最开始的测试用例,而是新建了一个DemoClz来做的,那么为什么这样选择呢,如果直接用第二节的测试用例会怎样呢?

public class BaseMain {
    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }
    public void run() {
        int i = 1;
        while (true) {
            i = print(i);
        }
    }
    public static void main(String[] args) {
        BaseMain main = new BaseMain();
        main.run();
}
复制代码


依然通过 jvm 参数指定 agent 的方式,运行上面的代码,会发现抛异常,无法正常运行了

image.png


指出了在 run 方法这里,存在字节码的错误,我们统计耗时的 Agent,主要就是在方法开始前和结束后各自新增了一行代码,我们直接补充在 run 方法中,则相当于下面的代码


image.png


上面的提示很明显的告诉了,最后一行语句永远不可能达到,编译就存在异常了;那么问题来了,作为一个 java agent 的提供者,我哪知道使用者有没有写这种死循环的方法,如果应用中有这么个死循环的任务存在,把我的 agent 一挂载上去,导致应用都起不来,这个锅算谁的????


下面提供解决方案,也很简单,在 jvm 参数中,添加一个-noverify (请注意不同的 jdk 版本,参数可能不一样,我的本地是 jdk8,用这个参数;如果是 jdk7 可以试一下-XX:-UseSplitVerifier)


在 IDEA 开发环境下,如下配置即可

image.png

再次运行,正常了

image.png


4. 小结


本篇为实战项目,首先明确方法参数Instrumentation它的接口定义,通过它来实现 java 字节码的修改


我们通过实现自定义的ClassFileTransformer,借助 javassist 来修改字节码,为每个方法的第一行和最后一行注入耗时统计的代码,从而实现方法耗时统计


最后留一个小问题,上面的实现中,当方法内部抛出异常时,我们注入的最后一行统计耗时会不会如期输出,如果不会,应该怎么修改,欢迎各位大佬留言指出解决方案



相关文章
|
29天前
|
存储 Java 索引
Java快速入门之数组、方法
### Java快速入门之数组与方法简介 #### 一、数组 数组是一种容器,用于存储同种数据类型的多个值。定义数组时需指定数据类型,如`int[]`只能存储整数。数组的初始化分为静态和动态两种: - **静态初始化**:直接指定元素,系统自动计算长度,如`int[] arr = {1, 2, 3};` - **动态初始化**:手动指定长度,系统给定默认值,如`int[] arr = new int[3];` 数组访问通过索引完成,索引从0开始,最大索引为`数组.length - 1`。遍历数组常用`for`循环。常见操作包括求和、找最值、统计特定条件元素等。
|
3天前
|
存储 安全 算法
Java容器及其常用方法汇总
Java Collections框架提供了丰富的接口和实现类,用于管理和操作集合数据。
Java容器及其常用方法汇总
|
5天前
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
20 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
|
3天前
|
Java API
java.time常用方法汇总
`java.time` API 是从 Java 8 开始引入的时间日期处理库,旨在替代老旧的 `java.util.Date` 和 `Calendar`。它提供了更简洁、强大和灵活的方式处理日期、时间、时区及时间间隔,支持全球化和时间计算需求。API 包含获取当前时间、创建指定时间、解析和格式化字符串、进行加减运算、比较时间、获取年月日时分秒、计算时间间隔、时区转换以及判断闰年等功能。示例代码展示了如何使用这些功能,极大简化了开发中的时间处理任务。
|
25天前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
22天前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
55 9
|
27天前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
59 12
|
28天前
|
算法 Java API
Java 方法注释:规范、实用和高质量的写法
本文深入探讨了如何编写高质量的 Java 方法注释
49 11
|
28天前
|
SQL Java 数据库连接
【潜意识Java】Java中JDBC过时方法的替代方案以及JDBC为什么过时详细分析
本文介绍了JDBC中一些常见过时方法及其替代方案。
41 5
|
1月前
|
存储 Java BI
java怎么统计每个项目下的每个类别的数据
通过本文,我们详细介绍了如何在Java中统计每个项目下的每个类别的数据,包括数据模型设计、数据存储和统计方法。通过定义 `Category`和 `Project`类,并使用 `ProjectManager`类进行管理,可以轻松实现项目和类别的数据统计。希望本文能够帮助您理解和实现类似的统计需求。
101 17

热门文章

最新文章