
一条命令定位线上问题:Arthas 的 watch 到底在 JVM 里做了什么
关键词标签:Arthas、JVM 字节码增强、线上诊断、watch 命令、Instrumentation
一个反直觉的事实
线上出问题时,我们最常做的动作是"加日志、重启、复现"。但 JVM 从 JDK 5 起就内置了 java.lang.instrument 这套能力,允许在不重启、不改代码、不重新打包的前提下,动态修改已经加载到内存里的字节码。Arthas 的绝大多数命令,本质上都是这套 Instrumentation API 的上层封装。
换句话说:你以为需要"发版才能排查"的问题,JVM 早就给了你一条后门。绝大多数人只是没用过而已。
本文不重复讲 Arthas 怎么装、怎么连(这些官方文档写得很清楚)。我们直接钻进 watch 这条命令,看它从"敲下回车"到"打印出方法入参"之间,JVM 里究竟发生了什么。假设我们有一个示例服务 order-service,其中有个方法 OrderService.createOrder(Long userId, String sku) 在生产环境行为异常,我们要在不重启的前提下看清它的入参和返回值。
一、watch 不是"监听",而是"运行时织入"
1.1 先看它到底做了什么
最常用的形式:
watch com.example.order.OrderService createOrder \
'{params, returnObj, throwExp}' -x 2
-x 2 表示展开对象层级为 2 层,{params, returnObj, throwExp} 是 OGNL 表达式,分别对应方法入参数组、返回值、抛出的异常。
很多人以为 watch 是"挂个钩子等事件",其实不是。它的真实流程是:
- 通过
Instrumentation.retransformClasses重新定义目标类的字节码; - 在目标方法前后插入 Arthas 自己的
AdviceListener调用(基于 ASM 生成); - 方法被执行时,这些被织入的字节码把
params、returnObj等塞进Advice对象; - 监听器按 OGNL 表达式求值,把结果回传给 Arthas 客户端。
关键点在于第 1 步:retransformClasses 是 JDK 官方 API,不是 Arthas 私有的黑魔法。
1.2 JDK 的 retransformClasses 有硬性约束
java.lang.instrument.Instrumentation#retransformClasses 的 Javadoc 里明确写了几条限制,这直接决定了 Arthas 能干什么、不能干什么:
- 不能新增方法、不能改方法签名、不能改字段。只能改方法体(以及常量池的部分内容)。
- 不能修改父类。
- 已经被 retransform 过的类可以再次 retransform,但每次都是基于原始字节码重新做增强,而不是在增强结果上叠加。
这解释了一个常见困惑:为什么 watch 只能观察方法,而不能"给类加个新方法"?因为 Instrumentation 的语义就卡在这里。想加方法,那得走 redefineClasses,而它同样不允许改签名,只是允许替换方法体实现,限制更死。
1.3 增强后的方法长什么样
Arthas 用 ASM 在方法体里插入的骨架,大致等价于下面这种结构(示意,非 Arthas 真实源码):
public Order createOrder(Long userId, String sku) {
Object[] params = new Object[]{userId, sku};
// before 回调
Spy.ON_BEFORE(adviceId, clazz, method, params);
Object result = null;
Throwable exp = null;
try {
// 原始方法体
result = doCreateOrder(userId, sku);
return (Order) result;
} catch (Throwable t) {
exp = t;
throw t;
} finally {
// after / throwing 回调
Spy.ON_AFTER(adviceId, clazz, method, params, result, exp);
}
}
Spy 是 Arthas 的入口类,它把事件分发给对应的 AdviceListener。注意 finally 块:无论方法正常返回还是抛异常,回调都会触发 ,这正是 watch 能同时捕获 returnObj 和 throwExp 的原因。
二、为什么 watch 有时"看不到"某些调用
2.1 内联(inlining)与 JIT 的干扰
JIT 在 C2 编译时,可能把被 watch 的小方法内联进调用方 。此时你在方法入口织入的字节码,在已经编译好的机器码里根本不存在------因为那个方法体已经被"展开"到调用方里去了。
Arthas 对此的处理是:对目标类做 retransform 时,JVM 会使该类已编译的机器码失效 ,让后续调用回退到解释执行或重新编译。所以通常你 watch 之后第一次调用会走新逻辑,但要注意:
- 如果目标方法已经被内联进其他类(调用方)的机器码里,光 retransform 被调方不一定能立刻生效,因为调用方那侧的机器码没被作废。
- 这也是为什么有时需要配合
-b(在方法执行前观察)或对调用方也做处理。
2.2 watch 的 -b / -e / -s 语义
这三个参数经常被混用:
| 参数 | 触发时机 | 此时 returnObj 是否可用 |
|---|---|---|
-b (before) |
方法体执行前 | 否,只有 params |
-s (success) |
方法正常返回后 | 是 |
-e (exception) |
方法抛异常后 | 否,只有 throwExp |
-f (finish) |
无论成功失败 | 成功时有 returnObj,失败时有 throwExp |
默认是 -f。如果你只关心耗时,用 -b 记录开始时间、-s 记录结束时间,就能算出单次调用耗时------这就是 trace 命令的雏形。
2.3 一个真实的坑:params 里的对象被改过
watch 打印的是引用 。如果方法内部修改了入参对象的字段,你在 -s(成功返回后)阶段看到的 params,可能已经是被修改后 的状态,而不是调用时的原始值。想抓原始入参,得用 -b。
# 只抓调用时的原始入参,避免被方法体污染
watch com.example.order.OrderService createOrder \
'{params[0], params[1]}' -b -x 2
三、watch 的 OGNL 表达式:能力与边界
3.1 你能拿到什么
在表达式里可用的变量(Arthas 官方文档列出的)包括 params、returnObj、throwExp、target(被调用的实例)、clazz、method、cost(耗时,毫秒)等。cost 尤其有用------它让你不必额外写 trace 就能筛出慢调用:
# 只打印耗时超过 200ms 的调用
watch com.example.order.OrderService createOrder \
'{params, returnObj, cost}' \
'#cost > 200' -x 2
#cost > 200 是条件表达式(condition),只有为真时才输出。这在线上高频方法上非常关键:不加条件地 watch 一个每秒被调上万次的方法,会把应用自己拖垮。
3.2 OGNL 的常见误区
params是Object[],取值用params[0],不是params.0。- 想调方法:
params[0].toString()、returnObj.getOrderId()都可以,但不要在里面写有副作用的调用,否则会污染业务状态。 - 对象层级默认
-x 1,深层嵌套对象只打印引用地址,得靠-x 3之类展开。层级越深,序列化开销越大。
四、watch 之外:一套组合拳
单条命令能解决一部分问题,但真实排查往往是组合:
trace:定位调用链上哪个环节慢,底层同样是字节码增强 +Spy回调,只是它在方法前后都插桩并记录时间戳。stack:看某方法是被谁调用的,本质是增强时抓取当前线程栈。jad:反编译已加载的类,确认线上跑的字节码和你以为的是否一致(热部署踩坑时尤其有用)。redefine:配合jad拿到源码、改完、再编译成 class 热替换。注意它同样受 Instrumentation 的签名约束。
这些命令共享同一套底层:Instrumentation + ASM 织入 + Spy 分发。理解了 watch 的机制,其余的只是"在什么时机、抓什么数据"的差异。
五、什么时候别用 / 别踩的坑
- 别在高频方法上裸
watch。每条命中都要走 OGNL 求值 + 序列化 + 回传,开销不小。务必加#cost > N之类的条件,或用-n限制次数。 - 别指望
watch能改行为 。它是只读观察 。想改返回值得用tt(TimeTunnel)配合-p回放,或直接热替换字节码。 - 别忽略 retransform 的失效边界。被内联进调用方的机器码不会因被调方 retransform 而失效,观察不到属正常现象,不是 Arthas 坏了。
- 别在生产上开
-x 5打印大对象 。深层展开会触发大量反射和toString,可能瞬间拉高 GC 压力。 - 别把 Arthas 当长期监控 。它是诊断工具,用完即退。持续观测应该交给 Micrometer + Prometheus 这类方案,而不是常驻的增强。
- 注意 JDK 版本差异 。
Instrumentation的能力随 JDK 演进有调整,某些老版本对 retransform 的支持更保守,跨大版本使用前建议对照目标 JDK 的java.lang.instrument文档确认。
小结与预告
watch 这条命令的价值,不在于"能看入参"这个表层功能,而在于它揭示了 JVM 一个被严重低估的能力:运行时字节码增强 。理解了 retransformClasses 的约束、Spy 的分发模型、以及 JIT 内联带来的观察盲区,你在用 Arthas 时就不再是"碰运气敲命令",而是知道每一步为什么生效、什么时候会失效。
本系列前面聊过线程池、AQS、CompletableFuture 这些并发底座,也聊过 Spring Boot 自动配置的源码路径。接下来我们继续沿着"运行时"这条线,看看类加载器隔离 如何在热部署、SPI、以及 Arthas 自身的 Spy 注入中扮演关键角色------为什么有些增强能生效,有些却因为类加载器不同而"看不见"。感兴趣的话,欢迎关注后续。
参考来源:
- Java SE
java.lang.instrument.Instrumentation官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.instrument/java/lang/instrument/Instrumentation.html - Arthas 官方文档(watch 命令):https://arthas.aliyun.com/doc/watch.html