zoukankan      html  css  js  c++  java
  • 手把手教你实现一个方法耗时统计的 java agent

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

    前面有两篇铺垫博文,在博文《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 方式运行(具体操作和上面一样),输出如下

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

    到这里本文的 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 的方式,运行上面的代码,会发现抛异常,无法正常运行了

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

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

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

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

    再次运行,正常了

    4. 小结

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

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

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

    (具体解决方案可以在源码中获取哦,还有配套的测试 case,求支持,求赞,求关注 ❀)

    II. 其他

    0. 相关

    相关博文

    相关源码

    1. 一灰灰 Bloghttps://liuyueyi.github.io/hexblog

    一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

    2. 声明

    尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

    3. 扫描关注

    一灰灰 blog

    QrCode

  • 相关阅读:
    angular 组件间数据共享
    Linux 常用命令
    angular 子路由跳转出现Navigation triggered outside Angular zone, did you forget to call ‘ngZone.run() 的问题修复
    angular :ngIf 的else用法
    利用 filter 来去重
    webpack打包时删除console.log,和debugger
    git忽略而不提交文件的3种情形
    jenkins 构建日程表配置
    vue之多页面的开发
    vue-cli3使用jq
  • 原文地址:https://www.cnblogs.com/yihuihui/p/12509416.html
Copyright © 2011-2022 走看看