Arthas 诊断工具

从 attach 原理、命令体系到线上排障剧本,系统理解 Arthas

Posted by Ekko on May 21, 2026

这篇笔记围绕 Arthas 的 attach 机制、命令体系与线上排障路径展开,重点放在 JVM 观测、类加载确认、方法级观测以及运行时增强的使用边界。

Arthas 的价值不在于记住多少命令,而在于把运行中 JVM 的“猜测”转成“可验证的证据”:先看全局,再看线程和类加载,最后下钻到方法现场,并在必要时控制增强范围、持续时间与权限边界。

参考资料:

官方资料:Arthas 官方仓库Arthas 官方文档快速入门命令列表进阶使用FAQ

运维与安全:鉴权说明options后台异步任务Arthas Tunnel

全局与监控:dashboardthreadmonitor

类与反编译:classloaderjad

方法观测:watchtracett

高级增强:ognlvmtoolprofilerretransformredefine

[TOC]


1. Arthas 是什么

Arthas 是阿里开源的 Java 在线诊断工具

它最有价值的地方,不是“命令很多”,而是它把线上 JVM 的几个关键诊断能力串起来了:

  • 看 JVM 全局状态
  • 看线程 CPU 和堆栈
  • 看类是谁加载的、代码到底从哪个 jar 来
  • 在线观察方法入参、返回值、异常、耗时
  • 在不重启进程的情况下做有限度的字节码增强
  • 采样生成火焰图,定位热点
  • 在必要时做临时热修复

很多线上问题,用日志很难快速回答:

  • 为什么部署后的代码没有生效
  • 这个类到底是不是预期中的那个类
  • 某个请求为什么慢
  • 某个用户的数据为什么异常
  • 线程为什么打满了 CPU
  • 某个对象到底在不在内存里

Arthas 的价值,本质上是:

把“只能猜”变成“可以在线验证”。


2. 为什么 Arthas 好用

如果只看表面,Arthas 像一个命令行工具;但从能力结构上看,它实际上把 4 类能力统一到了一个入口里。

JVM 观测

典型命令:

  • dashboard
  • thread
  • jvm
  • memory
  • sysprop
  • sysenv
  • vmoption

这类命令回答的是:

  • 现在 JVM 是否健康
  • CPU、内存、GC、线程整体有没有异常
  • 当前运行环境和启动参数到底是什么

类和类加载排查

典型命令:

  • sc
  • sm
  • classloader
  • jad
  • dump

这类命令回答的是:

  • 某个类是否真的被加载了
  • 是哪个 ClassLoader 加载的
  • 实际运行字节码来自哪个 jar
  • 运行中的源码长什么样

方法级在线观测

典型命令:

  • monitor
  • watch
  • trace
  • stack
  • tt

这类命令回答的是:

  • 这个方法有没有被调用
  • 调用耗时高不高
  • 入参、返回值、异常到底是什么
  • 方法内部哪个子调用最慢
  • 某次调用现场能不能“录下来”回看

运行时操作与热修复

典型命令:

  • ognl
  • vmtool
  • retransform
  • redefine
  • mc
  • reset

这类命令已经不是“看”,而是进入“操作 JVM”的层次,所以风险也更高。

一句话总结:

  • dashboard/thread/jvm 解决“系统怎么了”
  • classloader/jad/sc 解决“代码到底是不是这份”
  • watch/trace/tt 解决“调用现场发生了什么”
  • profiler 解决“热点到底在哪”
  • retransform/redefine 解决“能不能临时改一下”

3. Arthas 的工作方式

很多人第一次用 Arthas,只记住了:

1
java -jar arthas-boot.jar

但真正要用稳,必须理解它在做什么。

attach 到目标 JVM

Arthas 常见使用方式是通过 arthas-boot.jaras.sh attach 到一个已经运行的 Java 进程。

这意味着它不是:

  • 重启应用后生效
  • 修改源码再部署

而是:

  • 直接连到正在跑的 JVM
  • 借助 Instrumentation / JVMTI 等能力做观测和增强

这也是它适合线上排障的核心原因。

一部分命令只是读取,一部分命令会增强字节码

这是必须明确区分的。

相对“读”的命令:

  • jvm
  • memory
  • thread
  • classloader
  • sc
  • sm

相对“动代码”的命令:

  • watch
  • trace
  • monitor
  • tt
  • retransform
  • redefine

后者很多都依赖 字节码增强。官方文档也明确提醒,watch/trace/monitor/tt 这类命令是通过插桩实现的。

这意味着:

  • 能力很强
  • 不是零成本
  • 不应该大面积、长时间、无约束地增强

Arthas 的强项是“在线验证”,不是“长期监控平台”

它适合:

  • 临时定位线上问题
  • 精准观察某个类、某个方法、某个线程
  • 在故障窗口里拿证据

它不适合直接替代:

  • APM
  • 指标平台
  • 全链路 tracing
  • 长期火焰图采集系统

更准确的定位是:

Arthas 是故障现场里的“诊断手术刀”,不是常驻全量观测平台。

attach 的前提与常见失败场景

attach 成功并不是默认前提,生产环境里经常先卡在这里。

根据官方 FAQ,至少要先确认下面几件事:

  • 当前执行 Arthas 的操作系统用户,与目标 Java 进程的启动用户一致
  • 目标 JVM 仍然可以响应 attach 机制,而不是已经假死
  • 容器或 Kubernetes 场景里,目标进程、命名空间与旧版本 Arthas 残留没有互相干扰

最常见的几个失败信号包括:

  • target process not responding or HotSpot VM not loaded
  • telnet: connect to address 127.0.0.1: Connection refused
  • 在 Docker / Kubernetes 中 attach pid=1 进程失败

排查顺序可以固定下来:

  1. 先确认用户身份是否一致,因为 JVM 只能 attach 同一用户下启动的 Java 进程
  2. 再用 jstack -l <pid> 验证目标进程是否还有响应;如果 jstack 都无法工作,Arthas 往往也无法 attach
  3. 再检查 Arthas 服务端口、旧版本残留以及容器内 pid=1 这类特殊场景

这一层没有打通时,后面的 watchtracett 都无从谈起。


4. 常见接入方式

arthas-boot.jar

最常用,也最适合临时排障。

1
2
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

特点:

  • 上手最快
  • 适合本机或跳板机临时 attach
  • 可以交互式选择目标进程

as.sh

适合 Linux / Unix / macOS 一键使用。

1
2
curl -L https://alibaba.github.io/arthas/install.sh | sh
./as.sh

一个非常实用的点是可以用 --select 按进程名选择目标应用,避免每次先手工找 PID。

java agent 方式

适合希望应用启动时就具备 Arthas 能力的场景。

但工程上要谨慎:

  • 它更适合受控环境
  • 要和发布流程、启动参数、权限管理一起设计

Spring Boot Starter

适合与应用一同启动,但这类方案通常更偏平台化、团队内规范化使用,不是日常临时排障的第一入口。


5. 先建立一张命令地图

如果没有脑图,Arthas 会越学越乱。

更好的记忆方式是按“排障顺序”理解,而不是按文档目录硬记。

第一层:先看全局

先回答“系统是否整体异常”:

  • dashboard
  • thread
  • jvm
  • memory

第二层:再看代码和类加载

再回答“是不是代码版本 / 类加载问题”:

  • sc
  • sm
  • classloader
  • jad

第三层:最后看方法现场

再回答“某次调用里具体发生了什么”:

  • monitor
  • watch
  • trace
  • stack
  • tt

第四层:必要时动手

最后才进入高风险动作:

  • ognl
  • vmtool
  • profiler
  • retransform
  • redefine

这个顺序很重要,因为大量线上误用 Arthas 的根源就是:

  • 还没确认是否真有必要
  • 就直接 watch * *
  • 或直接尝试热更新

6. 排障主线应该怎么走

真正在线上排问题时,推荐按下面顺序推进。

先看 dashboard

1
dashboard

它适合第一眼扫全局:

  • 哪些线程 CPU 高
  • heap / nonheap 是否异常
  • GC 情况大概怎样
  • JVM 内部线程是否活跃异常

官方文档特别提到,Java 8 之后还能看到 JVM 内部线程的 CPU 时间,比如:

  • JIT 编译线程
  • GC 线程
  • VM Thread

这个细节很有用,因为有时 CPU 高并不是业务线程高,而是:

  • GC 在忙
  • JIT 在忙

再看 thread

1
2
3
thread -n 5
thread -b
thread 123

这是线上最应该熟练掌握的命令之一。

它常用于:

  • 查看最忙的前 N 个线程
  • 查看指定线程栈
  • 找阻塞其他线程的线程

重点理解两点:

  1. thread -n 3 是快速找 CPU 热点线程
  2. thread -b 是快速找锁竞争 / 阻塞源头

官方文档还说明了它的 CPU 使用率计算是采样得来的,因此看到的是一个时间窗口内的结果,而不是某个永恒真值。

然后验证类加载和代码版本

最常见的线上错觉之一是:

  • “代码明明已经修改,为什么没有生效”

这时不要猜,直接查。

1
2
3
4
5
sc -d com.example.OrderService
sm com.example.OrderService
classloader -t
classloader -c 3d4eac69
jad com.example.OrderService

这组命令用来回答:

  • 类是否已加载
  • 哪个 ClassLoader 加载了它
  • code source 在哪个 jar
  • 实际运行中的反编译代码是什么

这里 classloader 特别重要,因为很多“代码不生效”不是部署失败,而是:

  • 同名类被多个类加载器加载
  • 应用实际运行的并不是预期中的那个 jar
  • Spring Boot fat jar / 容器模块化环境里类来源复杂

最后再进入方法级观察

如果已经确认:

  • JVM 确实在异常
  • 线程或类加载方向已经有线索

这时再用:

  • monitor
  • watch
  • trace
  • tt

去拿方法级证据。


7. 高频命令深度理解

这一节按“输入什么、可能看到什么、应当如何判断”的顺序展开。

注意:

  • 下面的输出都是 节选后的示意输出
  • 真实线上输出会更长,但字段含义和判断思路是一样的
  • 学习时更重要的是理解“为什么下一步要接这条命令”

dashboard: 先看大盘,不要一上来就盯某个方法

输入

1
dashboard -i 2000 -n 3

典型输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ID   NAME                          GROUP  PRIORITY  STATE      %CPU
34   http-nio-8080-exec-12         main   5         RUNNABLE   68
53   Timer-for-arthas-dashboard    system 5         RUNNABLE   1
-1   G1 Young RemSet Sampling      -      -         -          0.5

Memory           used   total   max    usage
heap             612M   1024M   1024M  59.7%
g1_old_gen       420M   768M    1024M  41.0%
metaspace        132M   140M    -1     94.2%

Runtime
os.name          Linux
java.version     17.0.12
systemload.average  8.42
processors       8
uptime           3d 4h

怎么解读

  • 如果看到某个业务线程 %CPU 很高,下一步去 thread -n
  • 如果看到 GC 线程高、heap 使用率高,优先怀疑内存压力和频繁 GC
  • 如果 metaspace 很高,留意类加载、动态生成类、代理类过多
  • 如果只是系统负载高,但 Java 线程 CPU 不高,还要排查机器层问题,不一定是 JVM 本身

它最适合什么时候用

  • 刚连上一台有问题的 JVM
  • 还不知道问题偏 CPU、内存、线程还是类加载
  • 想先拿“全局第一眼印象”

一句话:

dashboard 负责告诉你“问题大概在哪一层”,但不负责给最终答案。


thread: CPU 飙高、阻塞、锁竞争的第一入口

输入

1
thread -n 3

示例输出

1
2
3
4
5
6
7
"http-nio-8080-exec-12" Id=87 cpuUsage=68% RUNNABLE
    at com.example.order.OrderService.createOrder(OrderService.java:142)
    at com.example.order.OrderController.create(OrderController.java:58)

"HikariPool-1 housekeeper" Id=31 cpuUsage=3% TIMED_WAITING

"G1 Young RemSet Sampling" Id=-1 cpuUsage=1% RUNNABLE

怎么解读

  • 如果热点线程直接落在业务代码里,下一步通常是 tracewatch
  • 如果热点线程落在 JSON 序列化、正则、集合遍历、加解密等本地计算,优先怀疑 CPU 计算热点
  • 如果热点线程卡在数据库或 RPC 客户端,说明慢点更可能在 IO 或下游
  • 如果热点线程是 GC / JIT 内部线程,要回头结合 dashboard 和内存情况看

再下钻单个线程

输入:

1
thread 87

示例输出:

1
2
3
4
5
"http-nio-8080-exec-12" Id=87 RUNNABLE
    at java.util.HashMap.resize(HashMap.java:698)
    at java.util.HashMap.putVal(HashMap.java:661)
    at com.example.order.OrderAssembler.build(OrderAssembler.java:89)
    at com.example.order.OrderService.createOrder(OrderService.java:142)

解释:

  • 这时已经可以判断“慢点在本地对象组装”
  • 下一步比起去看数据库,更应该直接 trace 这个方法

查阻塞源头

输入:

1
thread -b

示例输出:

1
2
3
Found one Java-level deadlock:
"http-nio-8080-exec-9" Id=91 BLOCKED on java.lang.Object@6d06d69c
    owned by "http-nio-8080-exec-7" Id=88

解释:

  • thread -b 不是告诉你“谁被挡住了”,而是告诉你“谁挡了别人”
  • 排锁问题时,这条命令非常省时间

sc / sm / classloader / jad: 排代码版本与类加载问题的四件套

这一组命令最适合解决:

  • 代码为什么没生效
  • 这个类是不是被多个类加载器加载了
  • 当前看到的到底是不是线上运行的那份代码

sc -d: 先确认类到底是谁

输入:

1
sc -d com.example.order.OrderService

示例输出:

1
2
3
4
5
6
class-info com.example.order.OrderService
code-source /app/order-service.jar!/BOOT-INF/classes!/
name com.example.order.OrderService
isInterface false
class-loader +-org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8
classLoaderHash 7f9a81e8

怎么解读:

  • code-source 告诉你类来自哪个 jar 或目录
  • classLoaderHash 是后续 jadvmtoolognl 精准定位的重要参数
  • 如果这里显示的 jar 不是你预期的部署包,问题已经基本找到了

sm: 再确认方法名到底对不对

输入:

1
sm com.example.order.OrderService createOrder

示例输出:

1
2
com.example.order.OrderService
`- public com.example.order.dto.OrderDTO createOrder(com.example.order.dto.CreateOrderCmd)

怎么解读:

  • 这一步的核心不是看源码,而是确认方法真的存在
  • 很多人 watch 没结果,根因只是类名或方法名没写对

classloader: 类加载器问题别靠猜

输入:

1
classloader -t

示例输出:

1
2
3
4
+-BootstrapClassLoader
 +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@301ec38b
   +-com.taobao.arthas.agent.ArthasClassloader@472067c7
   +-org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8

解释:

  • Spring Boot 可执行 jar 常见的是 LaunchedURLClassLoader
  • 后续很多命令如果不指定类加载器,可能观察到的不是目标中的那一份类

再看这个类加载器到底加载了哪些路径:

输入:

1
classloader -c 7f9a81e8

示例输出:

1
2
3
jar:file:/app/order-service.jar!/BOOT-INF/classes!/
jar:file:/app/order-service.jar!/BOOT-INF/lib/spring-web-6.1.8.jar!/
jar:file:/app/order-service.jar!/BOOT-INF/lib/mysql-connector-j-8.4.0.jar!/

怎么解读:

  • 这一步最适合验证 fat jar 内部类路径是否符合预期
  • 如果怀疑某个版本的依赖没有真正进入运行时,这条命令很有用

jad: 看正在执行的代码,不看本地仓库的代码

输入:

1
jad -c 7f9a81e8 com.example.order.OrderService createOrder

示例输出:

1
2
3
4
5
public OrderDTO createOrder(CreateOrderCmd cmd) {
    this.checkParam(cmd);
    UserDTO user = this.userRemote.query(cmd.getUserId());
    return this.orderAssembler.build(cmd, user);
}

怎么解读:

  • 如果这里和预期中的代码不一样,先排查部署与类加载
  • 如果这里已经能看出方法很短,那慢点多半不在当前方法本体,而在子调用,下一步接 trace

monitor / watch / trace / stack / tt: 这组命令一定要结合输出理解

先给出一组简明对照:

  • monitor 看趋势
  • watch 看现场值
  • trace 看路径耗时
  • stack 看调用来源
  • tt 看录像回放

monitor: 先看这个方法最近是不是一直不正常

输入:

1
monitor -c 5 com.example.order.OrderService createOrder

示例输出:

1
2
3
timestamp            class                            method       total success fail avg-rt(ms) fail-rate
2026-06-07 20:10:05  com.example.order.OrderService  createOrder  18    16      2    342.7      11.11%
2026-06-07 20:10:10  com.example.order.OrderService  createOrder  21    21      0    298.4      0.00%

怎么解读:

  • avg-rt(ms) 高,说明这个方法整体慢
  • fail-rate 高,说明不仅慢,可能还有异常
  • 但它只告诉你“最近这段时间整体怎样”,不会告诉你某一笔请求具体传了什么

下一步怎么接:

  • 想看某次调用值,用 watch
  • 想看慢在哪个子调用,用 trace

watch: 看入参、返回值、异常,最直观

输入:

1
watch com.example.order.OrderService createOrder '{params, returnObj, throwExp}' -x 2

示例输出:

1
2
3
4
5
6
7
method=com.example.order.OrderService.createOrder
ts=2026-06-07 20:12:11; [cost=324.551ms]
@ArrayList[
  @Object[][isEmpty=false;size=1],
  @OrderDTO[id=98765, status=CREATED, amount=199.00],
  null,
]

怎么解读:

  • 第一项是 params
  • 第二项是 returnObj
  • 第三项是 throwExp
  • 如果第三项是 null,说明这次没抛异常

只看异常场景:

输入:

1
watch com.example.order.OrderService createOrder '{params[0], throwExp}' -e -x 2

示例输出:

1
2
3
4
@ArrayList[
  @CreateOrderCmd[userId=1001, skuId=2008, amount=null],
  java.lang.IllegalArgumentException: amount can not be null
]

这时候就已经能直接回答:

  • 报错的是哪种入参
  • 异常类型是什么

只看进入方法前的参数:

1
watch com.example.order.OrderService createOrder '{params}' -b -x 2

这很适合排查“方法内部修改了参数对象”这类问题。

trace: 这个方法里到底谁最慢

输入:

1
trace com.example.order.OrderService createOrder '#cost > 100'

示例输出:

1
2
3
4
5
`---ts=2026-06-07 20:15:48;thread_name=http-nio-8080-exec-12;id=87;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8
    `---[312.442ms] com.example.order.OrderService:createOrder()
        +---[5.122ms] com.example.order.OrderService:checkParam()
        +---[248.773ms] com.example.user.UserRemoteService:query()
        `---[36.090ms] com.example.order.OrderAssembler:build()

怎么解读:

  • 这里已经很直观地看到主要慢在 UserRemoteService.query()
  • 这比“拍脑袋猜数据库慢”要可靠得多

继续下一步怎么接:

  • 如果怀疑下游返回值异常,用 watchuserRemote.query 的返回值
  • 如果是全局热点,不只这一条链路慢,可以接 profiler

stack: 谁在调它

输入:

1
stack com.example.order.OrderService createOrder

示例输出:

1
2
3
4
5
ts=2026-06-07 20:18:03;thread_name=http-nio-8080-exec-15
@java.lang.Exception
    at com.example.order.OrderService.createOrder(OrderService.java:142)
    at com.example.order.OrderAppService.submit(OrderAppService.java:66)
    at com.example.order.OrderController.create(OrderController.java:35)

怎么解读:

  • 它不是看方法内部做了什么,而是看“这个方法从哪条链路被调起来的”
  • 适合排“为什么这个方法会被意外调用”这类问题

tt: 偶发问题先录下来,再慢慢筛

输入:

1
tt -t com.example.order.OrderService createOrder

示例输出:

1
2
3
4
5
Affect(class-cnt:1 , method-cnt:1) cost in 64 ms.
INDEX   TIMESTAMP                COST(ms)  IS-RET  IS-EXP  OBJECT
1000    2026-06-07 20:20:01      18.21     true    false   0x7a12bcd1
1001    2026-06-07 20:20:05      482.77    true    false   0x7a12bcd1
1002    2026-06-07 20:20:09      6.33      false   true    0x7a12bcd1

接着筛慢调用:

1
tt -s '#cost > 300'

示例输出:

1
2
INDEX   TIMESTAMP                COST(ms)  IS-RET  IS-EXP
1001    2026-06-07 20:20:05      482.77    true    false

再查看具体记录:

1
tt -i 1001

示例输出:

1
2
3
4
index=1001
params[0]=@CreateOrderCmd[userId=1001, skuId=2008, couponId=889]
returnObj=@OrderDTO[id=98766, status=CREATED]
throwExp=null

怎么解读:

  • tt 的最大价值是先把现场存下来
  • 很适合偶发问题、难复现问题、夜间问题

但一定克制:

  • 高并发高频方法不要长时间开着
  • 用完及时清理记录

方法观测这一组命令,真正怎么选

若只需要一张可执行判断表,可以直接按下面选择:

排查目标 优先命令 原因
这个方法最近是不是一直慢 / 一直报错 monitor 它给的是统计趋势
某一笔请求的参数、返回值、异常是什么 watch 它给的是单次现场值
慢在哪个内部子调用 trace 它给的是路径耗时
到底是谁调了这个方法 stack 它给的是上游调用来源
问题偶发,先录下来以后再分析 tt 它给的是调用录像

如果某个接口慢,一个非常实用的顺序是:

  1. monitor 看它是不是持续慢
  2. trace 看内部谁慢
  3. watch 看慢请求的关键参数
  4. 如果问题偶发,再 tt 录现场

ognl: 直接在目标 JVM 里执行表达式

输入

1
ognl '@java.lang.System@getProperty("java.version")'

示例输出

1
@String[17.0.12]

这说明:

  • 表达式已经在目标 JVM 里执行
  • 不是在本地 shell 里执行

再看 Spring Boot 里的静态 logger:

1
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@org.springframework.boot.SpringApplication@logger'

示例输出:

1
2
3
4
@Slf4jLocationAwareLog[
  name=@String[org.springframework.boot.SpringApplication],
  logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
]

怎么解读:

  • ognl 最适合快速验证静态字段、配置值、简单表达式结果
  • 它不应被当作在线任意改动运行时状态的手段

原则:

  • 优先读
  • 表达式短小
  • 明确类加载器

vmtool: 从 JVM 里抓实例,看对象到底还在不在

输入

1
vmtool --action getInstances --className org.springframework.context.ApplicationContext --limit 2

示例输出

1
2
3
@ApplicationContext[][
  @AnnotationConfigServletWebServerApplicationContext[id=application, startupDate=1749301200000],
]

怎么解读:

  • 这能快速证明容器上下文实例真的存在
  • 对排“对象是否还活着”“容器是否初始化成功”很有帮助

进一步执行表达式:

1
2
3
4
vmtool --action getInstances \
  --className org.springframework.context.ApplicationContext \
  --limit 1 \
  --express 'instances[0].getBeanDefinitionCount()'

示例输出:

1
@Integer[286]

再比如筛非 daemon 线程名:

1
vmtool --action getInstances --className java.lang.Thread --limit -1 --express 'instances.{? #this.daemon == false}.{name}'

示例输出:

1
2
3
4
5
@ArrayList[
  @String[main],
  @String[http-nio-8080-exec-12],
  @String[scheduling-1],
]

注意:

  • --limit 一定要带
  • 不要对超大对象类、热点类无脑抓全量

profiler: 想知道全局热点在哪,就上采样

输入

1
profiler start

采样一段时间后停止:

1
profiler stop --format html

示例输出

1
profiler output file: /Users/admin/arthas-output/20260607-202955.html

怎么解读:

  • 这说明火焰图结果已经落盘
  • 接下来应该打开结果文件,而不是继续盯终端

什么时候优先用它:

  • 只知道“CPU 高”,但还不知道热点函数在哪
  • 怀疑热点不是单一方法,而是整个系统多个调用栈共同导致

trace 的区别:

  • trace 更像手术刀,盯住某条调用链
  • profiler 更像热力图,先看全局谁最热

retransformredefine: 热修复前先分清边界

官方建议已经很明确:

优先使用 retransformredefine 更谨慎。

retransform

输入:

1
2
3
jad --source-only com.example.order.OrderService > /tmp/OrderService.java
mc /tmp/OrderService.java -d /tmp
retransform /tmp/com/example/order/OrderService.class

示例输出:

1
2
retransform success, size: 1, classes:
com.example.order.OrderService

再查看当前 entry:

1
retransform -l

示例输出:

1
2
Id  ClassName                       TransformCount  LoaderHash
1   com.example.order.OrderService  1               7f9a81e8

怎么解读:

  • retransform 不是“一次改完就忘”
  • 它会留下 entry,后续要能查、能删、能再次触发

redefine

输入:

1
redefine /tmp/com/example/order/OrderService.class

示例输出:

1
redefine success, size: 1

但它的限制必须记住:

  • 不能新增字段和方法
  • reset 对它无效
  • watch/trace/jad/monitor/tt 等命令会有冲突

所以工程建议仍然是:

  • 临时观测优先 watch/trace/tt
  • 临时修复优先 retransform
  • redefine 留给非常明确的场景

8. Advice 上下文与 OGNL 条件表达式

Arthas 很多命令之所以强,不是因为命令名本身,而是因为它们共享了一套运行时上下文和表达式能力。

watchtracestackmonitor 这类命令里,最常用的 Advice 上下文变量包括:

  • params
  • returnObj
  • throwExp
  • target
  • this
  • #cost

这些变量对应的含义通常是:

  • params: 方法参数
  • returnObj: 返回值
  • throwExp: 抛出的异常
  • target: 当前目标对象
  • #cost: 执行耗时

因此可以把很多高频命令理解成“命令骨架 + 条件表达式 + 输出表达式”的组合:

1
2
3
watch com.example.UserService queryUser '{params, returnObj}' -x 2
watch com.example.UserService queryUser '{params, throwExp}' -e -x 2
trace com.example.UserService queryUser '#cost > 100'

表达式能力带来的直接收益是:

  • 可以精准过滤
  • 可以只看有价值的请求
  • 可以避免把终端刷爆

但表达式也会带来额外风险:

  • 写错表达式会让你误判
  • 过滤范围过大照样可能影响 JVM

更稳妥的原则是:

先缩小类,先缩小方法,再缩小条件,最后才展开对象层级。

另外,options 里的几个全局开关需要知道边界:

  • strict 默认是 true,目的是避免在 OGNL 表达式里误把判断写成赋值
  • json-format 可以让 watchtt 等输出更结构化,但不会自动解决“大对象展开过深”的问题
  • unsafe 默认是 false,只有在确实需要增强 java.*Bootstrap ClassLoader 加载的类时才考虑打开

这些开关会直接影响表达式行为和增强范围,线上环境不应随意调整。


9. 四类典型线上问题怎么排

CPU 飙高

推荐路线:

  1. dashboard 看整体 CPU 和 JVM 内部线程状态
  2. thread -n 5 找最忙线程
  3. thread <id> 看线程栈
  4. 必要时 profiler start/stop 做火焰图
  5. 如果已经怀疑某方法,再用 trace

经验判断:

  • GC 线程高: 优先看内存 / 分配问题
  • 业务线程高: 优先看具体调用栈
  • JIT 线程活跃异常: 留意最近是否大量增强 / 热更新

官方 dashboard 文档还特别指出,执行 trace/watch/tt/redefine 等命令后,JIT 线程活动可能变频繁,因为相关 class 的 JIT 编译结果会受影响,这个细节很容易被忽略。

“代码没生效”

推荐路线:

  1. sc -d com.xxx.YourClass
  2. classloader -t
  3. classloader -c <hash>
  4. jad com.xxx.YourClass

需要确认的不是“本地代码是否修改过”,而是:

  • JVM 当前加载的是哪份 class
  • 来自哪个 jar
  • 由哪个类加载器加载
  • 运行中的反编译代码是不是预期版本

这类问题,80% 不是 Arthas 问题,而是部署 / 类加载问题。

某用户数据异常,但线下复现不了

推荐路线:

  1. watch 目标方法的关键入参和异常
  2. 如果问题偶发,用 tt -t 录调用
  3. tt -s 过滤特定用户参数
  4. 必要时用 watch 只观察异常路径

这类场景最忌讳:

  • 一开始就无条件展开巨大的参数对象
  • 把全量请求都打印出来

更合理的做法是:

  • 先按用户 id / 订单号 / trace id 过滤
  • 再观察局部字段

怀疑内存里存在异常对象或容器状态有问题

推荐路线:

  1. sc -d 先确认类和类加载器
  2. vmtool --action getInstances ... --limit N
  3. 配合 --express 做进一步筛选
  4. 只在必要时 forceGc

这类场景最常见误区是:

  • 还没确定目标类,就直接全量抓实例

结果通常是:

  • 输出过大
  • JVM 压力升高
  • 证据也没拿清楚

Spring Boot 线上排障案例版

这一节把命令串成一个更真实的 Spring Boot 案例流程。

场景

线上 POST /api/orders/create 偶发很慢,RT 从平时的 80ms 抖到 500ms 以上。

现象:

  • 不是所有请求都慢
  • 没有明显报错
  • 研发怀疑是数据库、远程用户服务、或者新版本代码未生效

第一步:先确认是不是 JVM 整体异常

1
2
dashboard -i 2000 -n 3
thread -n 5

可能出现的输出:

1
"http-nio-8080-exec-12" Id=87 cpuUsage=68% RUNNABLE

判断:

  • 不是纯 GC 问题
  • 已经能定位到某个业务线程在忙

第二步:确认当前运行的是哪份 Spring Boot 代码

1
2
3
sc -d com.example.order.OrderService
classloader -t
jad -c 7f9a81e8 com.example.order.OrderService createOrder

可能出现的输出:

1
2
code-source /app/order-service.jar!/BOOT-INF/classes!/
class-loader +-org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8

判断:

  • 运行的确实是当前 Spring Boot fat jar 里的类
  • 类加载器也明确是 Boot 常见的 LaunchedURLClassLoader
  • 可以排除“看错类”“部署错包”这条线

第三步:先看这个方法最近整体是不是持续变慢

1
monitor -c 5 com.example.order.OrderService createOrder

可能出现的输出:

1
2
total  success  fail  avg-rt(ms)  fail-rate
21     21       0     318.4       0.00%

判断:

  • 当前没有异常爆发
  • 但 RT 的确显著升高

第四步:看慢在哪个子调用

1
trace com.example.order.OrderService createOrder '#cost > 100'

可能出现的输出:

1
2
3
4
`---[312.442ms] com.example.order.OrderService:createOrder()
    +---[5.122ms] com.example.order.OrderService:checkParam()
    +---[248.773ms] com.example.user.UserRemoteService:query()
    `---[36.090ms] com.example.order.OrderAssembler:build()

判断:

  • 主要慢点不在本地组装,也不在参数校验
  • 主要耗时落在远程用户服务 UserRemoteService.query()

第五步:再看慢请求传了什么参数、拿到了什么结果

1
watch com.example.user.UserRemoteService query '{params, returnObj, throwExp}' -x 2

可能捕获到的慢调用:

1
2
3
4
5
6
ts=2026-06-07 20:36:02; [cost=247.220ms]
@ArrayList[
  @Object[][isEmpty=false;size=1],
  @UserDTO[id=1001, level=VIP, region=HZ],
  null,
]

判断:

  • 下游不是报错
  • 而是“成功但慢”

这一步之后,排障方向已经可以从“代码逻辑错误”收敛到“远程依赖慢”。

第六步:问题偶发,再把现场录下来

1
2
3
tt -t com.example.order.OrderService createOrder
tt -s '#cost > 300'
tt -i 1001

可能出现的记录:

1
2
params[0]=@CreateOrderCmd[userId=1001, skuId=2008, couponId=889]
returnObj=@OrderDTO[id=98766, status=CREATED]

判断:

  • 可以进一步判断是不是特定用户、特定商品、特定优惠券组合才慢
  • 对偶发问题非常关键,因为这意味着故障现场已经被保留下来

如果怀疑不是远程调用慢,而是本地 CPU 热点

就换路线:

1
2
profiler start
profiler stop --format html

如果火焰图显示热点在:

  • JSON 序列化
  • 大对象拷贝
  • HashMap 扩容
  • 加解密

那就说明这次慢并不是下游慢,而是应用本地 CPU 热点。

这个案例说明了什么

Arthas 的价值不是“命令很多”,而是它能把 Spring Boot 线上排障从拍脑袋变成有步骤地收敛:

  1. dashboard / thread 看全局和热点线程
  2. sc / classloader / jad 确认当前运行代码
  3. monitor 判断是不是持续慢
  4. trace 找内部慢点
  5. watch 看具体参数和返回值
  6. tt 录偶发问题现场
  7. profiler 看全局 CPU 热点

这条链路的意义在于,把 Spring Boot 接口排障从“并行猜测”收敛成“逐层证实”。


10. 生产环境使用原则

先小范围,再扩大

错误示范:

1
watch * * '{params,returnObj}' -x 4

正确思路:

  • 先锁定类
  • 再锁定方法
  • 再加条件
  • 最后才考虑提高 -x

对象展开层级要克制

-x 很方便,但层级一大:

  • 输出会爆炸
  • 序列展开成本上升
  • 很容易把关键线索淹没

默认建议:

  • 先用 -x 1
  • 不够再到 -x 2
  • 真的必要再继续升

后台任务别开太多

Arthas 支持异步任务:

1
2
3
4
5
trace com.example.OrderService createOrder &
jobs
fg 1
bg 1
kill 1

这对“问题不知道何时出现”很有用,但官方也明确提醒:

  • 不要同时开启过多后台异步命令
  • 输出重定向虽然方便,但不是零成本

能鉴权就鉴权

如果需要远程访问 Web Console / HTTP API,认证配置一定要上。

官方支持:

  • --username
  • --password
  • arthas.properties

默认还有 arthas.localConnectionNonAuth=true 这种“本地连接可不鉴权”的便捷配置,但这个便利不能直接照搬到远程环境。

尤其是 HTTP API,官方推荐走标准 Authorization: Basic ... 头。

数据敏感性要有意识

watchttognlvmtool 很可能看到:

  • 用户手机号
  • token
  • 密码摘要
  • 身份证号
  • 订单金额

所以实践上要注意:

  • 只看必要字段
  • 输出文件要受控
  • 故障结束后及时清理

resetstopquit 不要混淆

要有基本边界感:

  • quit: 退出当前会话
  • stop: 停止 Arthas 服务
  • reset: 重置增强类,撤销大多数增强效果

但要特别记住:

  • resetredefine 不生效
  • retransform 也有自己的 entry 生命周期

所以不能想当然地以为“退出了就一定恢复原样”。

strictunsafejson-format 要分清使用边界

options 虽然只是全局开关,但它们直接影响表达式能力和增强范围。

生产环境里最值得关注的是:

  • strict=true 作为默认值通常应保持不变,它能避免把 OGNL 条件误写成对象属性赋值
  • unsafe=true 只在必须增强 JDK 类或 Bootstrap ClassLoader 加载类时才考虑打开,而且应尽快回到默认值
  • json-format=true 更适合结构化输出和日志留存,但不意味着可以放松对象展开层级控制

把这些开关当作“能力放大器”更合适,而不是默认常开项。


11. 最值得优先掌握的命令

如果时间有限,先把下面这组学熟,价值最高。

命令 优先级 解决的核心问题
dashboard 很高 JVM 当前整体是否异常
thread 很高 CPU 高、阻塞、线程栈
sc 很高 类是否加载、来源是什么
classloader 很高 类加载器问题、jar 来源问题
jad 很高 运行中的代码到底长什么样
watch 很高 入参、返回值、异常现场
trace 很高 方法内部到底哪里慢
monitor 方法层面的统计趋势
tt 偶发问题录现场
profiler 全局热点与火焰图
vmtool 中高 内存对象实例排查
ognl 中高 静态字段 / 表达式验证
retransform 临时热修复
redefine 更高风险热替换

12. 一套更合理的学习顺序

很多人学习 Arthas 的问题,不是不会命令,而是顺序错了。

更好的学习路径是:

先会看

先掌握:

  • dashboard
  • thread
  • jvm
  • memory

目标:

  • 能看懂 JVM 大盘

再会认代码

再掌握:

  • sc
  • sm
  • classloader
  • jad

目标:

  • 能确认运行时代码和类加载来源

再会抓现场

再掌握:

  • monitor
  • watch
  • trace
  • stack
  • tt

目标:

  • 能在线抓到方法调用证据

最后再碰高风险命令

最后再掌握:

  • ognl
  • vmtool
  • profiler
  • retransform
  • redefine

目标:

  • 在理解风险边界的前提下精细排障

这个顺序的好处是:

  • 先建立正确问题分层
  • 不会一开始就陷入表达式和热更新细节

13. 一份最小排障清单

线上接到一个 Java 问题时,可以先按这份最小清单推进。

全局确认

1
2
3
4
dashboard -i 2000 -n 3
thread -n 5
jvm
memory

类和代码确认

1
2
3
4
sc -d com.example.YourClass
sm com.example.YourClass
classloader -t
jad com.example.YourClass

方法现场确认

1
2
3
monitor -c 5 com.example.YourClass yourMethod
watch com.example.YourClass yourMethod '{params,returnObj,throwExp}' -x 2
trace com.example.YourClass yourMethod '#cost > 100'

偶发问题保留现场

1
2
tt -t com.example.YourClass yourMethod
tt -l

热点问题进一步下钻

1
2
profiler start
profiler stop --format html

14. 常见误区

误区:Arthas 就是在线 debug

不准确。

它更像:

  • 在线观测
  • 在线诊断
  • 有限度的运行时增强

它不是传统 IDE 单步调试器。

误区:命令执行了就一定没成本

不对。

特别是:

  • watch
  • trace
  • monitor
  • tt
  • vmtool
  • profiler

都要考虑目标 JVM 成本。

误区:jad 看到的就一定能重新编译

不一定。

反编译代码主要用于阅读理解,不要把它等同于原始源码。

误区:reset 一下就都恢复了

不绝对。

尤其涉及:

  • redefine
  • retransform

时,要按它们各自机制恢复。

误区:问题不确定,就把后台任务一直挂着

这很危险。

后台任务是应急手段,不是长期常驻方案。


15. 总结

如果把 Arthas 学成一堆零散命令,它会很快忘。

真正有效的理解方式应该是:

  • 先看全局
  • 再看线程
  • 再看类加载和运行时代码
  • 再看方法级调用现场
  • 最后才做对象抓取、采样分析、热修复

也就是说,Arthas 真正教会我们的不是几个命令,而是一种 线上问题分层定位方法

  1. 先确认系统级症状
  2. 再确认代码与类加载真相
  3. 再拿方法级证据
  4. 最后才使用高风险动作

如果压缩成一句结论,可以写成:

Arthas 最强的地方,不是能“看很多东西”,而是能在运行中的 JVM 里,以尽量小的代价,快速拿到正确证据。