JVM Profiler 方法耗时采集

简介: 开篇 JVM Profile的方法采集通过修改字节码在原来方法体的前置和后置增加采集耗时的代码。核心是基于基于java自带的instrument包和javassist包来实现的。

开篇

 JVM Profile的方法采集通过修改字节码在原来方法体的前置和后置增加采集耗时的代码。核心是基于基于java自带的instrument包和javassist包来实现的。

整个核心逻辑如下

  • 得到用户传入需要拦截的方法列表。
  • 在方法前后织入前置和后置耗时统计代码。
  • 内部保存耗时然后上报耗时。


源码分析

  • durationProfilingFilter 保存需要采集耗时的方法列表。
  • argumentFilterProfilingFilter 保存需要采集方法参数的方法列表。
public class JavaAgentFileTransformer implements ClassFileTransformer {
    private static final AgentLogger logger = AgentLogger.getLogger(JavaAgentFileTransformer.class.getName());

    private ClassAndMethodFilter durationProfilingFilter;
    private ClassMethodArgumentFilter argumentFilterProfilingFilter;

    /**
     *
     * @param durationProfiling 是采集的类和接口封装的对象
     * @param argumentProfiling 是
     */
    public JavaAgentFileTransformer(List<ClassAndMethod> durationProfiling, List<ClassMethodArgument> argumentProfiling) {
        this.durationProfilingFilter = new ClassAndMethodFilter(durationProfiling);
        this.argumentFilterProfilingFilter = new ClassMethodArgumentFilter(argumentProfiling);
    }
}

转码过程

  • 获取类的字节码并解析类的method。
  • 判断method是否在采集列表当中,如果不在采集列表就直接返回。
  • 针对所有需要被采集耗时的方法通过transformMethod进行字节码修改。
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            // 用于对原有的类进行转换并返回新的字节码
            if (className == null || className.isEmpty()) {
                logger.debug("Hit null or empty class name");
                return null;
            }
            return transformImpl(loader, className, classfileBuffer);
        } catch (Throwable ex) {
            logger.warn("Failed to transform class " + className, ex);
            return classfileBuffer;
        }
    }

    private byte[] transformImpl(ClassLoader loader, String className, byte[] classfileBuffer) {
        // 判断监控列表是否为空
        if (durationProfilingFilter.isEmpty()
                && argumentFilterProfilingFilter.isEmpty()) {
            return null;
        }

        // 转换类名替换"/"为"."
        String normalizedClassName = className.replaceAll("/", ".");
        logger.debug("Checking class for transform: " + normalizedClassName);

        // 判断是否在监控列表当中
        if (!durationProfilingFilter.matchClass(normalizedClassName)
                && !argumentFilterProfilingFilter.matchClass(normalizedClassName)) {
            return null;
        }

        byte[] byteCode;

        logger.info("Transforming class: " + normalizedClassName);

        try {
            // ClassPool首先负责加载该类然后判断该类的方法是否在拦截列表当中
            ClassPool classPool = new ClassPool();
            classPool.appendClassPath(new LoaderClassPath(loader));
            final CtClass ctClass;
            try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(classfileBuffer)) {
                ctClass = classPool.makeClass(byteArrayInputStream);
            }
            
            CtMethod[] ctMethods = ctClass.getDeclaredMethods();
            for (CtMethod ctMethod : ctMethods) {
                boolean enableDurationProfiling = durationProfilingFilter.matchMethod(ctClass.getName(), ctMethod.getName());
                List<Integer> enableArgumentProfiler = argumentFilterProfilingFilter.matchMethod(ctClass.getName(), ctMethod.getName());

                // 修改方法的字节码
                transformMethod(normalizedClassName, ctMethod, enableDurationProfiling, enableArgumentProfiler);
            }

            // 返回类的二进制编码
            byteCode = ctClass.toBytecode();
            // ClassPool中删除该类
            ctClass.detach();

        } catch (Throwable ex) {
            ex.printStackTrace();
            logger.warn("Failed to transform class: " + normalizedClassName, ex);
            byteCode = null;
        }

        return byteCode;
    }

方法增加前后置采集代码

  • 通过method.insertBefore()在原有的方法体前置添加耗时采集代码。
  • 通过method.insertAfter()在原有的方法体后置添加耗时采集代码
    // 对类进行前后拦截
    private void transformMethod(String normalizedClassName, CtMethod method, boolean enableDurationProfiling, List<Integer> argumentsForProfile) {
        if (method.isEmpty()) {
            logger.info("Ignored empty class method: " + method.getLongName());
            return;
        }

        if (!enableDurationProfiling && argumentsForProfile.isEmpty()) {
            return;
        }

        try {
            // 在方法中增加局部计时变量 startMillis_java_agent_instrument 和 durationMillis_java_agent_instrument
            if (enableDurationProfiling) {
                method.addLocalVariable("startMillis_java_agent_instrument", CtClass.longType);
                method.addLocalVariable("durationMillis_java_agent_instrument", CtClass.longType);
            }

            // 添加前置拦截代码
            StringBuilder sb = new StringBuilder();
            // 代码块的开始标志 "{"
            sb.append("{");

            // 添加开始采集程序执行开始时间的代码
            if (enableDurationProfiling) {
                sb.append("startMillis_java_agent_instrument = System.currentTimeMillis();");
            }

            // 如果该方法的某个参数需要被采集,添加采集代码
            for (Integer argument : argumentsForProfile) {
                if (argument >= 1) {
                    // 在javassist当中$1表示执行中第一个参数的值,这里根据传进来的下标argument来标识监控那个具体参数
                    // collectMethodArgument负责对接口参数的值进行监控
                    sb.append(String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodArgument(\"%s\", \"%s\", %s, String.valueOf($%s));}catch(Throwable ex){ex.printStackTrace();}",
                            normalizedClassName,
                            method.getName(),
                            argument,
                            argument));
                } else {
                    // 如果等于0那么采用其他方法进行采集
                    sb.append(String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodArgument(\"%s\", \"%s\", %s, \"\");}catch(Throwable ex){ex.printStackTrace();}",
                            normalizedClassName,
                            method.getName(),
                            argument,
                            argument));
                }
            }
            // 添加代码块结尾
            sb.append("}");

            // 在原有方法体之前插入前置代码块
            method.insertBefore(sb.toString());

            if (enableDurationProfiling) {
                // 在原有方法体之后插入后置代码块
                // collectMethodDuration负责采集接口耗时
                method.insertAfter("{" +
                        "durationMillis_java_agent_instrument = System.currentTimeMillis() - startMillis_java_agent_instrument;" +
                        String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodDuration(\"%s\", \"%s\", durationMillis_java_agent_instrument);}catch(Throwable ex){ex.printStackTrace();}", normalizedClassName, method.getName()) +
                        // "System.out.println(\"Method Executed in ms: \" + durationMillis);" +
                        "}");
            }

            // 整个函数体的执行代码增加了前置和后置处理,实现了耗时的监控
            logger.info("Transformed class method: " + method.getLongName() + ", durationProfiling: " + enableDurationProfiling + ", argumentProfiling: " + argumentsForProfile);
        } catch (Throwable ex) {
            ex.printStackTrace();
            logger.warn("Failed to transform class method: " + method.getLongName(), ex);
        }
    }
}


采集结果

{
    "metricName": "duration.min",
    "processName": "2203@xiaozhideMacBook-Pro.local",
    "appId": null,
    "host": "xiaozhideMacBook-Pro.local",
    "processUuid": "1e580f6e-0493-4e5b-bee2-a61c5f7b097d",
    "metricValue": 894.0,
    "methodName": "publicSleepMethod",
    "className": "com.uber.profiling.examples.HelloWorldApplication",
    "epochMillis": 1536072801037,
    "tag": "mytag"
}
目录
相关文章
|
4月前
|
缓存 监控 算法
吃透 JVM 诊断方法与工具使用
【8月更文挑战第4天】深入了解并掌握JVM诊断需把握几大要点:1) 熟悉JVM内存模型,如堆、栈及方法区;2) 掌握垃圾回收机制与算法;3) 运用工具如`jps`(查看Java进程)、`jstat`(监控运行状态)、`jmap`(生成堆快照)、`jhat`(分析堆快照)、`jstack`(检查线程栈); 4) 利用专业工具如Eclipse Memory Analyzer分析堆转储文件查找内存泄漏; 5) 动态监控与调整JVM参数; 6) 结合日志分析性能瓶颈。通过实战案例加深理解,有效应对JVM性能问题。
|
5月前
|
监控 安全 Java
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
132 2
|
5月前
|
缓存 Prometheus 监控
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
95 3
|
5月前
|
缓存 监控 算法
Java面试题:讨论JVM性能调优的常见方法和技巧。
Java面试题:讨论JVM性能调优的常见方法和技巧。
59 1
|
4月前
|
监控 Java Android开发
吃透 JVM 诊断方法与工具使用
【8月更文挑战第3天】要精通JVM诊断,需掌握关键监控指标如内存(堆/非堆)、CPU使用及线程状态;熟悉工具如`jstat`(监控状态)、`jmap`(堆转储)、`jstack`(线程堆栈);并能利用Eclipse Memory Analyzer (MAT)分析堆转储找内存泄漏;同时理解GC日志以优化垃圾回收行为;通过实践案例加深理解。
|
6月前
|
Java 编译器
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
56 1
|
5月前
|
JavaScript Java API
JAVA程序运行问题之JVM找到并开始执行main方法如何解决
JAVA程序运行问题之JVM找到并开始执行main方法如何解决
|
5月前
|
存储 设计模式 监控
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
48 0
|
7月前
|
存储 缓存 监控
JVM工作原理与实战(三十四):解决GC问题的方法
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了常见的垃圾回收(GC)模式、解决GC问题的方法(优化基础JVM参数、减少对象产生、更换垃圾回收器、优化垃圾回收器的参数)等内容。
130 0
|
7月前
|
存储 安全 Java
JVM之本地方法栈和程序计数器和堆
JVM之本地方法栈和程序计数器和堆
48 0