本文在飞书文档完成,所以导出为md格式有一些排版问题,另附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