Arthas源码拆解:动态诊断的底层原理
发布日期: 2026/07/28 阅读总量: 0

一句话破题

20:15 线上告警:用户下单接口TP99从200ms飙到8秒。登录机器,top看到CPU正常,堆内存正常,没有任何异常日志。你只能干瞪眼。

我当时的做法:1分钟启动Arthas,执行watch com.example.OrderService placeOrder '{params, returnObj}' -x 2,5秒后看到参数里一个数据库连接传成了null,触发重试死循环。定位解决总共3分钟。

如果靠传统加日志、重启、再复现,至少2小时。这就是Arthas的价值。今天从源码层面拆解它怎么做到的,以及你自己能不能写个简化版。

1. 问题:在线诊断的困境

Java应用诊断常见方式:

  • 远程调试:JDWP协议,需要启动时加参数,生产环境不安全,且断点会阻塞线程
  • 日志:需提前预埋,没埋点就抓瞎
  • jstack/jmap/jcmd:只能看堆栈/堆快照,无法查方法参数、局部变量
  • APM:采样率低,无法按需深入

Arthas通过动态附加(dynamic attach)到目标JVM,无需重启、无侵入、低开销,实现运行时代码热替换级别的诊断。

2. 方案对比:传统 vs Arthas

维度传统(加日志+重启)Arthas
启动前置需修改代码、重新部署零启动,attach即用
定位耗时(典型场景)30min~数小时1~5min
性能影响无(但增加日志IO)<3% (实际压测)1
安全性需鉴权(默认仅本机)
能力边界只能看日志打印方法参数、返回值、异常、局部变量、调用链路、火焰图

1: 基于8c16g实例,Arthas watch空方法,QPS 2000持续10分钟,TP99从100ms升至102ms

选择Arthas的本质原因:字节码增强技术使诊断能力按需插拔,零成本开启。

3. Arthas架构概览(基于v3.7.2)

Arthas分为三部分:

  • Client端:用户交互,telnet/http/websocket(源码在arthas-client模块)
  • Server端:启动入口,负责解析命令、管理会话(arthas-boot + arthas-core
  • Agent端:实际注入目标JVM的Java Agent(arthas-agent + arthas-spy

核心流程(源码时序图,略):

  1. 用户执行java -jar arthas-boot.jar,选择目标进程
  2. boot通过Attach APIcom.sun.tools.attach.VirtualMachine)加载agent.jar到目标JVM
  3. agent初始化:启动内部HTTP Server(默认8561端口),注册命令处理器,利用Instrumentation获取重新转换Class的能力
  4. 用户发送命令(如watch),agent解析后调用Enhancer对目标类进行字节码增强(使用ASM),插入切面逻辑
  5. 增强后的方法执行时,Spy切面采集数据,通过ResultSubscriber回传Client

4. 核心源码逐点解析

4.1 Attach与Agent加载

关键类:com.taobao.arthas.core.server.ArthasBootstrap(v3.7.2位于arthas-core模块)。

简化版attach代码(仅演示原理,不可用于生产):

// 来自 arthas-boot 模块,略作精简
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

String targetPid = "12345";
VirtualMachine vm = VirtualMachine.attach(targetPid);

// 设置arthas agent的属性
String agentJar = "/opt/arthas/arthas-agent.jar";
vm.loadAgent(agentJar, "args");

vm.detach(); // 加载成功后即可分离

:attach API在JDK9+因模块化限制,需要通过--add-opens开放。Arthas启动脚本已自动处理,但自己编写的诊断工具需注意。

4.2 InspectClass:初始化Transformer

agent启动后,核心是ArthasTransformer(实现ClassFileTransformer),注册到Instrumentation:

// 位于 com.taobao.arthas.core.enhance.ArthasTransformer
public byte[] transform(ClassLoader loader, String className,
                        Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                        byte[] classfileBuffer) throws IllegalClassFormatException {
    // 只处理需要增强的类,通过方法匹配规则过滤
    if (!match(className, loader)) {
        return null; // 返回null表示不修改
    }
    // 使用ASM读取并修改字节码
    ClassReader cr = new ClassReader(classfileBuffer);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
    EnhancerVisitor visitor = new EnhancerVisitor(cw, methodMatchers, spyClass);
    cr.accept(visitor, 0);
    return cw.toByteArray();
}

注意classBeingRedefined参数:当已经加载过的类需要重转换时,retransformClasses会调用此transform。
Arthas使用retransform而非redefine,因为retransform可以多次叠加转换器,且不影响类初始化状态。

4.3 ASM字节码增强:以watch命令为例

watch命令需在目标方法入口和出口插入切面。核心类SpyAdvice(位于arthas-spy模块):

// 简化版方法进入切面
public class SpyAdvice {
    public static void atEnter(ClassLoader classLoader, String className, 
                               String methodName, String methodDesc, Object target, 
                               Object[] args) {
        // 此处根据当前命令配置,决定是否采集参数
        // 采集结果写入 ThreadLocal 或队列,异步发送
        if (WatchPoint.isWatching(className, methodName)) {
            InvokeRecord record = new InvokeRecord();
            record.setArgs(args);
            record.setStartNano(System.nanoTime());
            WatchThreadLocal.current().push(record);
        }
    }
}

ASM生成的字节码会在目标方法首尾插入以下逻辑(伪代码表示):

// 目标方法编译后实际上变成了这样(简化版)
public void placeOrder(Order order) {
    SpyAdvice.atEnter(loader, "com.example.OrderService", "placeOrder", 
                      "(Lcom/example/Order;)V", this, new Object[]{order});
    try {
        order.validate();
        dao.save(order);
        SpyAdvice.atExit(loader, "placeOrder", null, null); // 正常返回
    } catch (Throwable e) {
        SpyAdvice.atExceptionExit(loader, "placeOrder", e);
        throw e;
    }
}

增强过程在com.taobao.arthas.core.enhance.asm.EnhancerVisitor中:遍历方法,检查方法注解和签名,对符合条件的生成以下AdviceAdapter。实际Arthas使用ASM5.0+。

4.4 命令接收与处理

用户输入watch com.example.OrderService placeOrder '{params, returnObj}' -x 2,命令解析在com.taobao.arthas.core.shell.system.impl.WatchCommand

// 伪代码
public class WatchCommand implements Command {
    @Override
    public void process(CommandProcess process) {
        // 解析参数:classPattern, methodPattern, conditionExpress, expand
        // 注册回调:ResultSubscriber,当SpyAdvice采集到数据时回调
        process.subscribe(new ResultSubscriber() {
            @Override
            public void onNext(ResultEntity result) {
                // 使用OGNL表达式计算 '{params, returnObj}'
                Object value = OgnlEvaluator.evaluate(expression, result);
                // 格式化输出
                process.appendResult(format(value));
            }
        });
        // 执行增强
        enhancer.enhance(classPattern, methodPattern, subscriber);
    }
}

OGNL表达式求值是灵活性的关键,也带来复杂度:Arthas内置了OgnlEvaluator,支持条件过滤(如'params[0].id == 10')。

5. 完整代码实现:手写简化版watch命令

下面用200行左右实现一个极简诊断工具,功能:对指定类和方法,在控制台打印参数和返回值。

分为三部分:Agent入口、ASM增强、测试程序。

5.1 Agent入口(MiniArthasAgent.java)

public class MiniArthasAgent {
    public static void agentmain(String args, Instrumentation inst) {
        // 解析参数:类名,方法名
        String[] parts = args.split(",");
        String className = parts[0].replace('.', '/');
        String methodName = parts[1];

        inst.addTransformer(new MiniTransformer(className, methodName), true);
        // 重转换所有已加载的类
        for (Class<?> clazz : inst.getAllLoadedClasses()) {
            if (clazz.getName().replace('.', '/').equals(className)) {
                try {
                    inst.retransformClasses(clazz);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.2 ASM Transformer(MiniTransformer.java)

public class MiniTransformer implements ClassFileTransformer {
    private final String targetClass;
    private final String targetMethod;

    public MiniTransformer(String targetClass, String targetMethod) {
        this.targetClass = targetClass;
        this.targetMethod = targetMethod;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (!className.equals(targetClass)) return null;

        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new MiniClassVisitor(Opcodes.ASM9, cw, targetMethod);
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

    static class MiniClassVisitor extends ClassVisitor {
        private final String targetMethod;
        public MiniClassVisitor(int api, ClassVisitor cv, String targetMethod) {
            super(api, cv);
            this.targetMethod = targetMethod;
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
            if (name.equals(targetMethod)) {
                return new MiniMethodVisitor(api, mv, access, name, desc);
            }
            return mv;
        }
    }

    static class MiniMethodVisitor extends AdviceAdapter {
        public MiniMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
        }

        @Override
        protected void onMethodEnter() {
            // 打印参数:直接System.out(简化)
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("[Enter] " + targetMethod);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            // 实际可用ASM输出局部变量,代码较长略
        }

        @Override
        protected void onMethodExit(int opcode) {
            if (opcode == RETURN || opcode == ARETURN) {
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("[Exit] " + targetMethod);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
        }
    }
}

注意:上述代码仅为演示,未处理异常、参数收集、线程安全等问题。Arthas使用更复杂的Spy机制避免干扰业务逻辑。

5.3 打包与测试

将上述代码打成jar(MANIFEST.MF指定Agent-Class: MiniArthasAgent)。

测试目标程序:TestService.java

public class TestService {
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }

    public static void main(String[] args) throws InterruptedException {
        TestService service = new TestService();
        while (true) {
            service.sayHello("world");
            Thread.sleep(1000);
        }
    }
}

启动后,通过attach执行:

java -cp /path/to/tools.jar:. MiniArthasAttach 12345 TestService sayHello

输出效果:

[Enter] sayHello
Hello world
[Exit] sayHello

6. 效果数据:Arthas vs 手写 vs 传统

场景传统定位(加日志重启)手写Agent(本简化版)Arthas
首次部署5min(改代码+编译+发布)1min(attach)30s(下载+启动)
查看方法参数需多行日志需定制代码一行命令
条件过滤需日志框架支持不支持OGNL表达式
性能开销(QPS 2000时TP99增值)无(但额外IO)约5%<3%
支持类数所有类仅单类任意多个类

压测环境:CentOS 7.6, JDK 8u202, 8c16g, 一个空方法循环。Arthas watch参数加-n 10限制采样10次。使用wrk压测100并发,每秒请求2000左右。

Arthas性能优异的原因:

  • 采样后异步处理,不阻塞业务线程
  • 使用环形缓冲区(RingBuffer)避免锁竞争
  • 只在匹配的类上插桩,且启用时才有开销

7. 避坑指南(亲历的坑)

  • 坑1:attach失败,提示“Can not find tools.jar”
    原因:JDK路径问题,Arthas需要tools.jar里的VirtualMachine。解决方案:确保JAVA_HOME指向JDK而非JRE。Arthas 3.7.2已支持JDK9+自动绕过。
  • 坑2:watch命令不生效,无输出
    可能原因:
    • 目标方法没有被调用(检查类名和方法名拼写,注意内部类用$分隔)
    • 方法的字节码被混淆(如ProGuard),需用实际混淆名
    • 类加载器隔离导致Arthas无法增强(如Tomcat的WebappClassLoader),Arthas会自动寻找合适的ClassLoader。
  • 坑3:生产环境使用Arthas导致应用Crash
    我遇到过:watch一个循环调用的方法,且-n设置太大,结果Spy切面的日志队列积压导致OOM。Arthas默认采集限制TABLE_CAPACITY=4096,但如果你执行watch ... -n 1000000,仍可能堆积。建议设置合理的采样次数,并监控Arthas自身内存。
  • 坑4:JDK9+模块化导致attach失败
    错误信息:java.lang.reflect.InaccessibleObjectException。Arthas启动脚本已处理,但如果你直接调API,需加JVM参数:--add-opens java.base/jdk.internal.module=ALL-UNNAMED
  • 坑5:重转换后类方法执行变慢
    虽然Arthas开销小,但某些场景(例如trace命令将每个方法调用都埋点),可能增加20%以上耗时。建议生产上诊断时,只对单个方法watch且限制次数。

8. 总结

Arthas的源码核心是Attach + Instrumentation + ASM + OGNL,加上一个轻量级的HTTP/telnet Server。读完本文你不仅能理解其原理,还能手写一个最小版本。下次线上问题,别加日志重启了,试试Arthas,你会发现新世界。

如果你对某个命令(如tracettstack)的内部实现感兴趣,评论区留言,我下篇写。