Arthas&Instrumentation初步学习

Jul 10, 2023 min read

本文在飞书文档完成,所以导出为md格式有一些排版问题,另附PDF文件,建议阅读PDF文件。

    / [pdf]

Arthas

Arthas 是 Alibaba 开源的 Java 诊断工具,用于在线解决生产问题,无需 JVM 重启,功能非常全面,包含性能监控、堆栈追踪、反编译、热更新等功能,具体可参考官方文档:https://arthas.aliyun.com/doc/

Arthas 支持的命令和功能如下:

其中功能非常丰富,这里简单介绍几个我认为比较有意思的功能。

dashboard & thread & memory

dashboard 显示当前实时数据面板,包含 thread、memory、gc、runtime 等信息

thread 显示查看当前线程信息和线程的堆栈,可以一键展示当前最忙的前 N 个线程并打印堆栈

vmoption & vmtool

vmoption 可以查看更新 VM 诊断相关的参数,比如开启 PrintGCDetails 等。

vmtool 可以查询内存对象并执行 express,强制 GC 、终止目标 Thread 等功能。

jad & mc & retransform

jad 基于 CFR 可以反编译指定已加载类的源码,例如反编译官方给出的一个质因数分解的案例:

mc 可以编译.java 文件生成.class

retransform 加载外部的.class 文件,retransform jvm 已加载的类,实现代码热更新,例如在此前的质因数分解中的代码中添加一行打印命令:

随后通过 mc 命令将该文件编译,使用 retransform 加载后,程序输出如下:

通过 jad & mc & retransform 三个命令结合可以实现线上应用的热更新。

trace

trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路,并且支持调用耗时过滤,trace 次数限制等功能

watch & monitor

watch 能方便的观察到指定函数的调用情况。能观察到的范围为:返回值抛出异常入参,通过编写 OGNL 表达式进行对应变量的查看。

monitor 能够执行方法执行监控

除了以上介绍的命令外,还有 tt(记录方法执行信息)、sc(查看 JVM 已加载的类信息)、profiler(集成 async-profiler 生成火焰图)等实用的功能。

Instrumentation

在上面的功能中,可以看到 Arthas 支持许多实用的分析功能,其中特别是热更新功能我认为很有意思。经过查阅资料,Arthas 主要基于 instrumentation api 实现 debug 和 profiler 能力。

那么什么是 instrumentation 呢?

The mechanism for instrumentation is modification of the byte-codes of methods.

instrumentation api 位于 java.lang.instrument 包下。但我们运行 Java 程序时,程序会首先转换成字节码,然后使用类加载器将它们加载到 JVM。使用 Instrumentation API,我们可以在运行时修改这些程序的字节码。

开发者可以基于 Instrumentation API 构建 Agent,用来监测和协助运行在 JVM 上的程序,可以理解成 Instrumentation API 提供了一种虚拟机级别的 AOP 方式,使得开发者无需对原有应用做任何修改,就可以实现类的动态修改和增强。

有两种方式可以

为了能够使用 instrumentation,我们必须首先加载我们的程序。

有两种方式可以加载:

  • static–启动时使用 -javaagent 选项运行,实现 premain 函数
  • dynamic–将代理 jar 包 attach 到目标 JVM 中,实现 agentmain 函数

这里简单介绍一个例子,目标程序不断打印字符串,我们目的是修改该输出方法,添加一些其他的打印信息。代码可以查看 https://github.com/icyclv/InstrumentationDemo

SimpleApp.java

public class SimpleApp {
    public static void main(String[] args) throws InterruptedException {
        while (true){
            doSomething();
            Thread.sleep(1000);
        }
    }

    public static void doSomething(){
        System.out.println("Doing something");
    }
}

为了实现两种方式,我们分别实现 agentmain 和 premain 函数

MyAgent.java

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        System.out.println("[Agent] In agentmain method");
        inst.addTransformer(new MyTransformer(), true);
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
            if (allLoadedClass.getSimpleName().equals("SimpleApp")) {
                inst.retransformClasses(allLoadedClass);
                break;
            }
        }

    }

    public static void premain(String args, Instrumentation instr) {
        System.out.println("[Agent] In premain method");
        instr.addTransformer(new MyTransformer());
    }
}

同时实现自定一个 Transformer 类,该类实现 ClassFileTransformer 接口并实现 transform 方法,在这个类中,我们将使用 javaassist 读取类的字节数组,并在其上添加我们自己的功能,从而修改程序的字节码。

MyTransformer.java

public class MyTransformer implements ClassFileTransformer {

        public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
                        ProtectionDomain protectionDomain, byte[] classfileBuffer)  {
                byte[] byteCode = classfileBuffer;

                //Add instrumentation to Sample class alone
                if (className.equals("com/cyc/InstrumentationDemo/SimpleApp")) {
                        try {
                                ClassPool classPool = ClassPool.getDefault();
                                CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                                CtMethod[] methods = ctClass.getDeclaredMethods();
                                for (CtMethod method : methods) {
                                        method.insertBefore("System.out.println(\"adding start line..\");");
                                        method.insertAfter("System.out.println(\"adding end line..\");");
                                }
                                byteCode = ctClass.toBytecode();
                                ctClass.detach();
                        } catch (Throwable ex) {
                                System.out.println("Exception: " + ex);
                                ex.printStackTrace();
                        }
                }
                return byteCode;
        }
}

需要注意,agent 程序打包时需要表明 premain 类等相关信息:

<archive>
  <manifest>
     <addDefaultImplementationEntries>
        true</addDefaultImplementationEntries>
     <addDefaultSpecificationEntries>
        true</addDefaultSpecificationEntries>
  </manifest>
  <manifestEntries>
    <Agent-Class>com.cyc.instrumentationDemo.agent.MyAgent</Agent-Class>
     <Premain-Class>com.cyc.instrumentationDemo.agent.MyAgent</Premain-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
 <Can-Retransform-Classes>true</Can-Retransform-Classes>
  </manifestEntries>
</archive>

通过以上打包方式,我们可以直接通过在程序启动时指定 javaaagent 的路径实现函数修改。而如果需要在程序启动后动态修改,则需要将 attach 到目标 jvm 上:

publpublic class MyAttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        System.out.printf("Attaching to VM with id: %s%n", args[0]);
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        try {
            vm.loadAgent(args[1]);
            System.out.println("Finished loading Agent");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            vm.detach();
        }
    }
}

从而实现方法的增强,效果如下:

以上代码可以查看 https://github.com/icyclv/InstrumentationDemo

参考

1.Arthas 文档:https://arthas.aliyun.com/doc/

2.Java Instrumentation — A Simple Working Example in Java https://medium.com/javarevisited/java-instrumentation-a-simple-working-example-in-java-a2c549024d9c

3.Guide to Java Instrumentation https://www.baeldung.com/java-instrumentation