Arthas 一个命令排查线上问题

一条命令定位线上问题: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 是"挂个钩子等事件",其实不是。它的真实流程是:

  1. 通过 Instrumentation.retransformClasses 重新定义目标类的字节码;
  2. 在目标方法前后插入 Arthas 自己的 AdviceListener 调用(基于 ASM 生成);
  3. 方法被执行时,这些被织入的字节码把 paramsreturnObj 等塞进 Advice 对象;
  4. 监听器按 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 能同时捕获 returnObjthrowExp 的原因。

二、为什么 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 官方文档列出的)包括 paramsreturnObjthrowExptarget(被调用的实例)、clazzmethodcost(耗时,毫秒)等。cost 尤其有用------它让你不必额外写 trace 就能筛出慢调用:

复制代码
# 只打印耗时超过 200ms 的调用
watch com.example.order.OrderService createOrder \
  '{params, returnObj, cost}' \
  '#cost > 200' -x 2

#cost > 200 是条件表达式(condition),只有为真时才输出。这在线上高频方法上非常关键:不加条件地 watch 一个每秒被调上万次的方法,会把应用自己拖垮。

3.2 OGNL 的常见误区

  • paramsObject[],取值用 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 的机制,其余的只是"在什么时机、抓什么数据"的差异。

五、什么时候别用 / 别踩的坑

  1. 别在高频方法上裸 watch 。每条命中都要走 OGNL 求值 + 序列化 + 回传,开销不小。务必加 #cost > N 之类的条件,或用 -n 限制次数。
  2. 别指望 watch 能改行为 。它是只读观察 。想改返回值得用 tt(TimeTunnel)配合 -p 回放,或直接热替换字节码。
  3. 别忽略 retransform 的失效边界。被内联进调用方的机器码不会因被调方 retransform 而失效,观察不到属正常现象,不是 Arthas 坏了。
  4. 别在生产上开 -x 5 打印大对象 。深层展开会触发大量反射和 toString,可能瞬间拉高 GC 压力。
  5. 别把 Arthas 当长期监控 。它是诊断工具,用完即退。持续观测应该交给 Micrometer + Prometheus 这类方案,而不是常驻的增强。
  6. 注意 JDK 版本差异Instrumentation 的能力随 JDK 演进有调整,某些老版本对 retransform 的支持更保守,跨大版本使用前建议对照目标 JDK 的 java.lang.instrument 文档确认。

小结与预告

watch 这条命令的价值,不在于"能看入参"这个表层功能,而在于它揭示了 JVM 一个被严重低估的能力:运行时字节码增强 。理解了 retransformClasses 的约束、Spy 的分发模型、以及 JIT 内联带来的观察盲区,你在用 Arthas 时就不再是"碰运气敲命令",而是知道每一步为什么生效、什么时候会失效。

本系列前面聊过线程池、AQS、CompletableFuture 这些并发底座,也聊过 Spring Boot 自动配置的源码路径。接下来我们继续沿着"运行时"这条线,看看类加载器隔离 如何在热部署、SPI、以及 Arthas 自身的 Spy 注入中扮演关键角色------为什么有些增强能生效,有些却因为类加载器不同而"看不见"。感兴趣的话,欢迎关注后续。

参考来源

相关推荐
字节探索1 小时前
别再只会写 Controller 了!一文吃透 Spring Boot 高级特性与项目实战
spring boot·spring
Kyrie_kk1 小时前
Java--ProcessBuilder操作系统进程
java·后端
SamDeepThinking1 小时前
关于java final关键字的可见性
java·后端·面试
Wang's Blog1 小时前
Java 项目实战: 外卖平台-MyBatis-Plus分页插件与员工分页查询
java·项目开发
MetaLite2 小时前
全网最好的SpringBoot接口请求对象设计
java·spring boot·后端
蜗牛互联网2 小时前
AI给面试打分不够,求职者更需要可核对的证据
java·人工智能·后端
此时不提桶,更待何时2 小时前
02-05-B-虚拟线程面试与生产事故实战
java·面试
名字还没想好☜2 小时前
Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理
java·后端·spring
摇滚侠3 小时前
《Spring Boot 3:高级与架构设计》第 2 章 IOC容器的高级机制 BeanFactoryPostProcessor 个人理解 6
java·spring boot·笔记·后端