Arthas线上故障排查实战手册
本手册面向 Java 后端开发与运维工程师,系统讲解如何使用 Alibaba Arthas 诊断线上问题。每个场景包含问题现象、排查思路、具体命令和解决方案,可作为线上排障的案头参考。
一、Arthas 简介
Arthas 是 Alibaba 开源的 Java 线上诊断工具,能够在不修改应用代码、不重启 JVM 的前提下,实时查看应用的线程状态、方法调用出入参、类加载信息、JVM 运行指标等,并支持动态修改运行时行为。
解决了什么痛点
生产环境出了问题,传统排查方式存在三大困境:
- 无法调试——生产环境不允许挂断点调试,因为
suspend所有线程会导致服务停顿。 - 难以复现——有些问题在测试环境无法复现,重启 JVM 后问题可能消失。
- 加日志成本高——修改代码→测试→预发→生产,整个流程耗时数小时,且问题可能已经不复现。
Arthas 通过字节码增强技术,以"观察者"身份注入运行中的 JVM,不暂停线程,不动业务代码,即可完成方法级观测与诊断。
核心能力一览
| 能力分类 | 对应命令 | 典型用途 |
|---|---|---|
| 全局监控 | dashboard |
实时查看线程、内存、GC 概览 |
| 线程诊断 | thread |
定位 CPU 高占用线程、死锁检测 |
| 方法观测 | watch |
查看方法入参、返回值、异常 |
| 调用链追踪 | trace |
定位方法内部耗时瓶颈 |
| 调用路径 | stack |
查看方法被谁调用 |
| 方法监控 | monitor |
统计方法调用次数、成功率、平均 RT |
| 时空隧道 | tt |
记录方法历史调用,事后回放 |
| 类反编译 | jad |
确认运行中加载的类是否为预期版本 |
| 类搜索 | sc / sm |
查找已加载的类与方法 |
| 字段查看 | getstatic / vmtool |
直接读取静态字段或堆内对象 |
| 动态执行 | ognl |
执行任意表达式,调用任意方法 |
| 内存编译 | mc |
编译 .java 为 .class |
| 热更新 | redefine / retransform |
运行时替换字节码 |
| 性能分析 | profiler |
生成火焰图 |
| 日志管理 | logger |
动态修改日志级别 |
版本与环境要求
Arthas 4.x 支持 JDK 8+,包括 JDK 17、JDK 21 和 JDK 25,覆盖 Linux/Mac/Windows 平台。采用命令行交互模式,支持 Tab 自动补全,同时支持 telnet 和 WebSocket 连接方式(本地与远程均可)。
二、安装与快速启动
2.1 快速安装
# 下载 arthas-boot.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动(会列出当前机器上的 Java 进程,选择要诊断的进程)
java -jar arthas-boot.jar
启动后会看到类似下面的进程列表:
[INFO] arthas-boot version: 4.x.x
[INFO] Found existing java process, please choose one and input the serial number:
* [1]: 12345 com.example.MyApplication
[2]: 23456 org.apache.catalina.startup.Bootstrap
输入序号即可 attach 到目标进程。
2.2 指定进程启动
如果已知进程 PID 或名称,可以直接指定,跳过选择步骤:
# 通过 PID 指定
java -jar arthas-boot.jar 12345
# 通过进程名指定(推荐,因为 PID 每次重启会变)
java -jar arthas-boot.jar --select MyApplication
2.3 Docker 环境中使用
容器环境下,Arthas 需要进入目标容器的命名空间:
# 方式一:进入容器后执行
docker exec -it <container_id> /bin/bash
java -jar arthas-boot.jar
# 方式二:使用 docker exec 直接运行(需容器内已有 JDK)
docker exec -it <container_id> java -jar /path/to/arthas-boot.jar
对于精简镜像(如基于 Alpine 且无 JDK 的镜像),可以将 Arthas 的 agent jar 挂载进去,或使用 arthas-spring-boot-starter 随应用一起打包。
2.4 通过 Web Console 远程连接
Arthas 启动后会同时监听本地 telnet(端口 3658)和 HTTP(端口 8563)。通过浏览器访问 http://<host>:8563 即可使用 Web Console,适合远程排查无法 SSH 的环境。
对于多服务器场景,可以部署 Arthas Tunnel Server 统一管理多个节点上的 Arthas 连接。
2.5 随应用启动(Spring Boot Starter)
在 pom.xml 中引入依赖,Arthas 会随应用自动启动:
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-spring-boot-starter</artifactId>
<version>4.x.x</version>
<scope>provided</scope>
</dependency>
建议设为 provided 或配合配置开关控制,避免在生产镜像中默认开启。
2.6 退出与卸载
# 退出当前 Arthas 会话(不影响应用运行)
quit
# 或
exit
# 彻底卸载 Arthas(清理增强的字节码)
stop
stop 命令会移除 Arthas 对 JVM 的所有增强,恢复原始字节码。生产排查完毕后建议执行 stop 确保环境干净。
三、核心命令体系总览
3.1 诊断流程方法论
面对线上问题,建议遵循"先全局、后局部,先现象、后根因"的排查路径:
dashboard(全局概览)
│
├─ CPU 高 → thread -n <N>(找最忙线程)→ thread <id>(看堆栈)→ jad(反编译确认)→ trace/watch(定位具体方法)
│
├─ 接口慢 → trace(追踪耗时链路)→ watch(观测入参出参)→ 找到瓶颈节点
│
├─ 异常报错 → watch(观测异常)→ stack(看调用来源)→ jad(确认代码逻辑)
│
├─ 内存/OOM → dashboard(看内存区域)→ vmtool(查堆内对象)→ heapdump(导出堆)
│
└─ 类加载问题 → sc(查类是否加载)→ jad(反编译看版本)→ dump(导出字节码比对)
3.2 命令分类速览
JVM 诊断类:
| 命令 | 作用 | 使用频率 |
|---|---|---|
dashboard |
实时仪表盘,展示线程、内存、GC、运行环境 | 极高 |
thread |
查看线程信息,定位 CPU 高占用、死锁 | 极高 |
jvm |
查看 JVM 详细信息(版本、启动参数、类加载器) | 中 |
memory |
查看各内存区域使用情况 | 高 |
sysprop |
查看/修改系统属性 | 中 |
sysenv |
查看环境变量 | 中 |
vmoption |
查看/修改 JVM 诊断参数 | 中 |
getstatic |
查看类的静态字段 | 高 |
vmtool |
从堆中查询指定类的实例对象 | 高 |
heapdump |
导出堆 dump 文件 | 中 |
方法诊断类:
| 命令 | 作用 | 使用频率 |
|---|---|---|
watch |
观察方法入参、返回值、异常,支持条件过滤 | 极高 |
trace |
追踪方法内部调用链路及各节点耗时 | 极高 |
stack |
输出方法被调用的调用栈 | 高 |
monitor |
统计方法调用次数、成功率、平均 RT | 高 |
tt |
时空隧道,记录并回放方法历史调用 | 中 |
类与字节码类:
| 命令 | 作用 | 使用频率 |
|---|---|---|
sc |
查看已加载的类信息 | 高 |
sm |
查看已加载类的方法信息 | 高 |
jad |
反编译指定类,查看实际运行的代码 | 极高 |
mc |
内存编译 .java 文件为 .class | 中 |
redefine |
热加载 .class 文件替换运行时类 | 中 |
retransform |
批量 retransform 已加载的类 | 低 |
dump |
导出已加载类的字节码到文件 | 中 |
classloader |
查看类加载器信息 | 中 |
其他工具类:
| 命令 | 作用 | 使用频率 |
|---|---|---|
ognl |
执行 OGNL 表达式,调用任意方法 | 高 |
logger |
查看/修改日志级别 | 高 |
profiler |
生成火焰图,性能分析 | 中 |
options |
全局选项配置 | 中 |
四、实战场景详解
场景一:CPU 飙高排查
问题现象
线上监控告警,某台服务器 CPU 使用率持续 95%+,但接口 QPS 并未明显增长。应用表现为整体卡顿,部分接口超时。
排查思路
CPU 飙高通常有两类原因:一是某个或某些线程在执行 CPU 密集型计算(如死循环、正则回溯、序列化大对象),二是频繁 Full GC 导致。需要先判断是哪一类,再定位到具体线程和代码。
详细步骤
第一步:全局概览
[arthas@12345]$ dashboard
重点关注两个区域:
- Threads 区域:观察哪些线程 CPU 占比高,状态是否为 RUNNABLE。
- Memory / GC 区域:观察是否频繁 Full GC。如果 Full GC 次数快速增长,说明可能是内存问题导致 CPU 飙高。
第二步:定位最忙线程
# 查看占用 CPU 最高的 3 个线程
[arthas@12345]$ thread -n 3
输出示例:
threads name cpu state
pool-1-thread-1 85.6% RUNNABLE
http-nio-8080-exec-3 8.2% RUNNABLE
GC thread 4.1% RUNNABLE
pool-1-thread-1 的 CPU 占用高达 85.6%,这就是问题线程。
第三步:查看线程堆栈
# 查看指定线程的完整堆栈
[arthas@12345]$ thread 1
输出示例:
"pool-1-thread-1" Id=32 cpu=85.6% RUNNABLE
at com.example.service.OrderService.calculatePrice(OrderService.java:128)
at com.example.service.OrderService.processOrder(OrderService.java:95)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
堆栈显示卡在 OrderService.calculatePrice 方法的第 128 行。
第四步:反编译确认代码
# 反编译确认运行中的代码逻辑
[arthas@12345]$ jad com.example.service.OrderService calculatePrice
输出会显示反编译后的源码,检查第 128 行附近的逻辑。如果发现是 while 循环条件判断有误导致死循环,就找到了根因。
第五步:追踪方法耗时(确认是否死循环)
# 追踪该方法内部调用,观察是否一直在循环
[arthas@12345]$ trace com.example.service.OrderService calculatePrice -n 5 --skipJDKMethod false
如果 trace 结果显示该方法执行时间极长且持续不返回,基本确认是死循环。
解决方案
- 临时止血:通过
thread -b检查是否有阻塞线程,必要时在代码中增加循环退出条件或超时机制。如果该线程是非关键业务线程,可以考虑通过thread暂停(不建议在生产直接操作)。 - 根因修复:修复死循环逻辑,重新发布。
- 如果是 Full GC 导致:转至场景四:内存泄漏与 OOM 排查。
常见 CPU 飙高根因清单
| 根因 | 特征 | 确认方式 |
|---|---|---|
| 死循环 | 单线程 CPU 持续 90%+,方法长时间不返回 | thread -n + trace |
| 正则回溯 | CPU 飙高但无明显业务异常,涉及正则匹配 | thread 堆栈可见 java.util.regex |
| 序列化大对象 | JSON/toString 循环引用或对象极深 | thread 堆栈可见 com.fasterxml.jackson 或 toString |
| 频繁 Full GC | GC 线程 CPU 高,dashboard 中 Full GC 次数快速增长 | dashboard 内存区域 |
| 加密/压缩 | 涉及 MessageDigest、Deflater 等 CPU 密集操作 |
thread 堆栈可见相关类 |
场景二:接口响应缓慢排查
问题现象
用户反馈订单查询接口从正常的 100ms 变慢到 3 秒以上,但 CPU 和内存使用率正常,无明显错误日志。
排查思路
接口慢的原因可能出在方法内部调用的任何一个环节:数据库查询、远程 RPC 调用、序列化、锁竞争等。需要用 trace 逐层追踪调用链,找到耗时最长的节点。
详细步骤
第一步:确定入口方法
假设慢接口对应的 Controller 方法为 com.example.controller.OrderController.queryOrder。
第二步:追踪调用链耗时
[arthas@12345]$ trace com.example.controller.OrderController queryOrder -n 5
输出示例:
`---[3205ms] com.example.controller.OrderController:queryOrder()
+---[0.15ms] com.example.dto.OrderRequest:validate()
+---[3012ms] com.example.service.OrderService:getOrder()
| `---[2980ms] com.example.mapper.OrderMapper:selectById()
| `---[2975ms] org.apache.ibatis.session.SqlSession:selectOne()
+---[180ms] com.example.service.PriceService:calculatePrice()
`---[12ms] com.example.converter.OrderConverter:toVO()
一目了然:OrderMapper.selectById 耗时 2980ms,占总耗时的 93%。问题出在数据库查询。
第三步:观测入参(确认是否为特定参数导致)
# 观察方法入参,展开 2 层
[arthas@12345]$ watch com.example.mapper.OrderMapper selectById "{params, returnObj}" -x 2
触发一次请求后可以看到:
method=com.example.mapper.OrderMapper.selectById params=[null]
入参为 null,说明上游传入的订单 ID 为空,可能导致数据库全表扫描。
第四步:进一步追踪上游
# 追踪 OrderService 中传入 mapper 的值来源
[arthas@12345]$ trace com.example.service.OrderService getOrder -n 3
可能发现是从缓存中取到的订单 ID 为 null,但缓存逻辑未做空值判断,直接透传给了数据库查询。
进阶:条件过滤追踪
当接口调用频繁,只想看慢于某个阈值的调用:
# 只追踪耗时超过 500ms 的调用
[arthas@12345]$ trace com.example.controller.OrderController queryOrder '#cost > 500' -n 5
#cost 是 Arthas 内置变量,表示方法执行耗时(毫秒)。
解决方案
| 瓶颈位置 | 解决方案 |
|---|---|
| 数据库查询慢 | 检查索引、SQL 执行计划;考虑加缓存 |
| 远程 RPC 调用慢 | 检查下游服务状态、网络延迟;增加超时与熔断 |
| 序列化耗时 | 检查是否有循环引用、大对象;优化序列化方式 |
| 锁竞争 | 通过 thread -b 查看阻塞线程;优化锁粒度 |
| 日志同步写盘 | 改为异步日志;检查日志级别是否过低 |
trace 命令关键参数说明
trace <class-pattern> <method-pattern> [条件表达式] [参数]
#cost 方法执行耗时(毫秒),可用于条件过滤
-n <N> 最多匹配次数,达到后自动停止(重要,避免持续增强)
--skipJDKMethod <true|false> 是否跳过 JDK 方法(默认 true)
-x <N> 展开层级(对 trace 不常用,主要用于 watch)
-v 打印条件表达式的执行结果
场景三:方法异常与空指针定位
问题现象
线上偶发 NullPointerException,但日志中没有完整的调用链路,无法确定是哪个方法返回了 null。错误日志只显示了一行 at com.example.service.CouponService.apply(CouponService.java:76),看不到上游传入的参数。
排查思路
需要用 watch 命令观测方法的入参、返回值和抛出的异常,找到导致 NPE 的具体数据。如果问题偶发,还需要结合条件过滤来精准捕获。
详细步骤
第一步:观测方法入参和异常
# 观测入参和抛出的异常,展开 3 层
[arthas@12345]$ watch com.example.service.CouponService apply "{params, throwExp}" -x 3
触发业务请求后输出:
method=com.example.service.CouponService.apply params=[CouponRequest(userId=12345, couponId=null, amount=100.0)]
throwExp=java.lang.NullPointerException: Cannot invoke "Coupon.getType()" because "coupon" is null
关键信息:couponId 为 null,导致后续查询优惠券返回 null,调用 coupon.getType() 时 NPE。
第二步:只观测抛出异常的调用
如果方法正常调用居多,异常只是偶发,可以加条件过滤:
# 只在抛出异常时输出
[arthas@12345]$ watch com.example.service.CouponService apply "{params, throwExp}" "#throwExp != null" -x 3
#throwExp 是内置变量,表示方法抛出的异常对象。条件表达式 #throwExp != null 确保只在异常发生时才输出。
第三步:查看调用来源
# 查看是谁调用了 apply 方法
[arthas@12345]$ stack com.example.service.CouponService apply -n 3
输出示例:
ts=2026-07-15 10:30:45; thread_name=http-nio-8080-exec-5; id=45; is_daemon=false; priority=5;
@com.example.controller.OrderController.createOrder()
at com.example.service.OrderService.processOrder(OrderService.java:89)
at com.example.service.CouponService.apply(CouponService.java:70)
确认调用链是 OrderController.createOrder → OrderService.processOrder → CouponService.apply。
第四步:反编译确认代码逻辑
[arthas@12345]$ jad com.example.service.CouponService apply
检查反编译后的代码,确认第 70-76 行的逻辑——可能是查询优惠券后未做 null 检查就直接调用方法。
解决方案
- 立即修复:在
CouponService.apply中增加空值检查,优惠券不存在时返回友好提示而非 NPE。 - 防御性编程:上游
OrderService在传入 couponId 前应校验非空。 - 临时热修复:如果无法立即重新发布,可参考场景八:热更新代码紧急修复。
watch 命令核心参数
watch <class-pattern> <method-pattern> <观察表达式> [条件表达式] [参数]
观察表达式中可用的内置变量:
params 方法入参数组
returnObj 方法返回值
throwExp 方法抛出的异常
target 方法所在对象实例
#cost 方法执行耗时(ms)
常用参数:
-x <N> 展开层级,-x 1 只显示一层,-x 3 展开三层嵌套对象
-n <N> 匹配次数上限
-b 在方法调用前观察(此时 returnObj 和 throwExp 为 null)
-e 在方法抛出异常后观察
-s 在方法返回后观察
-f 在方法结束后观察(无论正常返回或异常,默认行为)
场景四:内存泄漏与 OOM 排查
问题现象
应用每隔 2-3 天就会 OOM 重启,重启后恢复正常。GC 日志显示 Old 区持续增长,Full GC 后内存回收不明显。
排查思路
内存泄漏的典型特征是:对象被持续创建但无法被 GC 回收。需要先确认是哪个内存区域泄漏,再找到泄漏的具体对象类型,最后定位到持有这些对象的引用链。
详细步骤
第一步:查看内存区域使用情况
[arthas@12345]$ memory
输出示例:
Memory used total max usage
heap 1.2G 2.0G 4.0G 30.42%
non_heap 256M 320M -1 -
g1_eden 120M 160M -1 75.00%
g1_old 1.0G 1.6G 4.0G 63.13%
g1_survivor 32M 48M -1 66.67%
g1_old 区使用率 63%,如果这个值持续增长且 Full GC 后不下降,说明老年代有对象无法回收。
第二步:通过 dashboard 观察 GC 趋势
[arthas@12345]$ dashboard
观察 Memory 区域的 GC 次数和耗时趋势。如果 Full GC 频率越来越高但 Old 区不降,确认是内存泄漏。
第三步:查询堆中对象实例
# 查询堆中某个类的实例对象,限制 10 个
[arthas@12345]$ vmtool --action getInstances --className com.example.model.OrderCache --limit 10
输出会显示找到的实例及其内容。如果发现 OrderCache 实例数量异常多(比如几万个),说明这些对象无法被回收。
第四步:查看对象引用链(关键)
# 查看实例被谁引用(找到 GC Root 引用链)
[arthas@12345]$ vmtool --action getInstances --className com.example.model.OrderCache --limit 1 --express 'instances[0]'
进一步可以查看持有这些对象引用的字段:
# 查询某个缓存类的实例
[arthas@12345]$ vmtool --action getInstances --className java.util.concurrent.ConcurrentHashMap --limit 1 \
--express 'instances[0].size'
如果发现某个 ConcurrentHashMap 的 size 达到了几十万,且 key 是不断增长的订单数据,而该 Map 没有清理机制,就是泄漏源头。
第五步:导出堆 dump 做离线分析
# 导出完整堆 dump
[arthas@12345]$ heapdump /tmp/heapdump.hprof
将 dump 文件下载到本地,用 MAT (Memory Analyzer Tool) 或 JProfiler 进行离线分析,查看 Dominator Tree 和 GC Root 引用链,精确定位泄漏对象。
第六步:查看静态字段确认缓存类
# 查看某个缓存管理类的静态字段
[arthas@12345]$ getstatic com.example.cache.OrderCacheManager cacheMap
或使用 vmtool 查询缓存管理器实例并查看其内部 Map 的大小。
解决方案
| 泄漏场景 | 典型表现 | 解决方案 |
|---|---|---|
| 缓存无上限 | Map/List 持续增长 | 使用 Caffeine/Guava Cache 设置最大容量和过期策略 |
| ThreadLocal 未清理 | 线程池中 ThreadLocal 数据残留 | 在 finally 中调用 ThreadLocal.remove() |
| 监听器未注销 | 事件监听器持续累积 | 在对象销毁时注销监听器 |
| 静态集合引用 | 静态 Map 持有对象引用 | 避免静态集合持有业务对象;定期清理 |
| 数据库连接未关闭 | 连接池耗尽 + 连接对象泄漏 | 使用 try-with-resources;检查连接归还逻辑 |
内存排查命令对比
| 命令 | 适用场景 | 优势 |
|---|---|---|
dashboard |
快速判断哪个内存区域异常 | 全局概览,一目了然 |
memory |
精确查看各内存区域数值 | 数据准确,按区域细分 |
vmtool |
查询堆中特定类的实例 | 在线分析,无需导出 dump |
getstatic |
查看静态字段值 | 直接读取,适合缓存类排查 |
heapdump |
导出完整堆做离线分析 | 可用 MAT 做深度引用链分析 |
场景五:动态修改日志级别
问题现象
线上某个接口偶发异常,但当前日志级别为 INFO,缺少 DEBUG 级别的详细日志。重新发布修改日志级别成本太高且会导致问题可能无法复现。
排查思路
Arthas 的 logger 命令可以在运行时动态修改日志级别,无需重启应用。这是线上排障最高频的操作之一。
详细步骤
第一步:查看当前日志框架和级别
[arthas@12345]$ logger
输出示例:
name level classLoader classLoaderHash
ROOT INFO BootClassLoader 5a54a66
com.example INFO BootClassLoader 5a54a66
com.example.service.OrderService INFO BootClassLoader 5a54a66
org.springframework WARN BootClassLoader 5a54a66
org.mybatis DEBUG BootClassLoader 5a54a66
可以看到 com.example 的日志级别是 INFO。
第二步:修改日志级别为 DEBUG
# 修改指定包的日志级别
[arthas@12345]$ logger --name com.example --level DEBUG
修改后,com.example 包下所有类的 DEBUG 日志会立即开始输出。
第三步:针对特定类修改
# 只对某个类开启 DEBUG
[arthas@12345]$ logger --name com.example.service.OrderService --level DEBUG
第四步:排查完毕后恢复
# 恢复为 INFO
[arthas@12345]$ logger --name com.example --level INFO
注意事项
logger命令支持 Logback、Log4j、Log4j2 三种主流日志框架,会自动识别。- 如果应用使用了自定义的日志配置(如编程式配置),可能无法通过
logger命令修改,此时可以用ognl直接调用日志框架的 API:
# 通过 OGNL 直接调用 Logback API 修改级别
[arthas@12345]$ ognl '@org.slf4j.LoggerFactory@getLogger("com.example").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
场景六:类冲突与类加载问题排查
问题现象
应用启动正常,但运行时抛出 NoSuchMethodError 或 ClassCastException。明明本地代码没问题,线上却报错。怀疑是不同版本的 jar 包冲突,加载了错误版本的类。
排查思路
类冲突问题的核心是确认 JVM 实际加载的是哪个版本的类、来自哪个 jar 包。需要用 sc 查看类加载信息,用 jad 反编译确认代码版本,用 dump 导出字节码做比对。
详细步骤
第一步:查找已加载的类
# 查找所有匹配的已加载类(支持通配符)
[arthas@12345]$ sc -d com.example.dto.OrderRequest
输出示例:
class-info com.example.dto.OrderRequest
code-source /data/app/lib/example-dto-1.2.0.jar
name com.example.dto.OrderRequest
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name OrderRequest
modifier public
annotation
interfaces
super-class java.lang.Object
class-loader +-sun.misc.Launcher$AppClassLoader@5a54a66
+-sun.misc.Launcher$ExtClassLoader@1234abcd
classLoaderHash 5a54a66
关键信息是 code-source:example-dto-1.2.0.jar。如果预期应该是 1.3.0 版本,说明打包时引入了旧版本。
第二步:检查是否存在多个版本
# 查看所有匹配的类(可能存在多个版本被不同 ClassLoader 加载)
[arthas@12345]$ sc -d *OrderRequest*
如果看到两条记录来自不同的 jar 包版本,确认是版本冲突。
第三步:反编译确认代码差异
# 反编译查看实际运行的代码
[arthas@12345]$ jad com.example.dto.OrderRequest
检查反编译后的代码是否包含预期的新方法。如果旧版本缺少某个方法,就会导致 NoSuchMethodError。
第四步:导出字节码做比对
# 导出运行中的字节码到文件
[arthas@12345]$ dump com.example.dto.OrderRequest -d /tmp/dump
导出后可以用 javap -p 查看字节码,或与预期版本的 jar 包做 diff 比对。
第五步:查看类加载器树(复杂场景)
# 查看所有类加载器
[arthas@12345]$ classloader
输出示例:
name loadedCount hash parent
BootstrapClassLoader 2845 null null
sun.misc.Launcher$ExtClassLoader 12 1234abcd BootstrapClassLoader
sun.misc.Launcher$AppClassLoader 3567 5a54a66 ExtClassLoader
org.springframework.boot.loader.LaunchedURLClassLoader 2341 abcdef12 AppClassLoader
在 Spring Boot fat jar 部署场景下,类加载器层级较深,需要确认类是从哪个加载器加载的。
解决方案
- Maven 依赖排查:使用
mvn dependency:tree | grep <artifactId>找到传递依赖的旧版本,用<exclusion>排除。 - 强制指定版本:在
pom.xml的<dependencyManagement>中锁定正确版本。 - 清理 lib 目录:如果是 lib 目录下存在多个版本的 jar,删除旧版本。
- 类加载器隔离:如果业务上确实需要多版本共存(如插件化架构),使用自定义 ClassLoader 做隔离。
场景七:线程死锁排查
问题现象
应用某个功能完全卡住,接口请求超时但无异常日志。CPU 使用率很低(说明线程都在等待而非计算),但功能不可用。
排查思路
死锁的典型特征是多个线程互相等待对方持有的锁,导致全部阻塞。Arthas 的 thread 命令可以一键检测死锁。
详细步骤
第一步:检测死锁
# 检测死锁(Arthas 会自动分析线程的锁等待关系)
[arthas@12345]$ thread -b
-b 参数会找出阻塞其他线程的线程。如果有死锁,会输出死锁环中的线程信息。
第二步:查看阻塞线程详情
# 查看指定线程的堆栈
[arthas@12345]$ thread 45
输出示例:
"http-nio-8080-exec-3" Id=45 WAITING on java.util.concurrent.locks.ReentrantLock@3f5a2b1c owned by "http-nio-8080-exec-5" Id=47
at java.lang.Object.wait(Native Method)
- waiting on java.util.concurrent.locks.ReentrantLock@3f5a2b1c
at com.example.service.InventoryService.deductStock(InventoryService.java:52)
- locked java.util.concurrent.locks.ReentrantLock@7c8d9e0f <---- 但 blocks "http-nio-8080-exec-5"
可以看到线程 45 在等待锁 3f5a2b1c(被线程 47 持有),同时持有锁 7c8d9e0f(线程 47 在等待这把锁)。这就是经典的死锁环。
第三步:查看所有阻塞线程
# 查看所有状态为 BLOCKED 的线程
[arthas@12345]$ thread --state BLOCKED
第四步:确认锁竞争代码
# 反编译查看死锁涉及的代码
[arthas@12345]$ jad com.example.service.InventoryService deductStock
检查代码中的锁获取顺序。典型场景:方法 A 先锁 resource1 再锁 resource2,方法 B 先锁 resource2 再锁 resource1。
解决方案
- 统一锁顺序:确保所有获取多把锁的代码路径使用相同的锁获取顺序。
- 使用 tryLock 超时:用
tryLock(timeout)替代lock(),避免永久阻塞。 - 减小锁粒度:用细粒度锁替代粗粒度锁,减少锁竞争。
- 使用并发工具类:用
ConcurrentHashMap、AtomicLong等替代手动加锁。
常见死锁类型
| 死锁类型 | 特征 | 检测方式 |
|---|---|---|
| 互斥锁死锁 | 多线程互相等待 ReentrantLock/synchronized | thread -b |
| 线程池耗尽 | 所有线程被占满,新任务排队等待 | thread 查看线程池线程状态 |
| 数据库锁等待 | 事务互相等待行锁 | 查看数据库 SHOW PROCESSLIST |
| 循环等待外部资源 | 等待 HTTP 响应/消息回复 | thread 堆栈可见 Socket.read |
场景八:热更新代码紧急修复
问题现象
线上发现一个紧急 bug:某个判断条件写反了导致用户无法正常下单。重新发布需要走 CI/CD 流程,预计 30 分钟以上,但每分钟都在损失订单。需要立即修复。
排查思路
Arthas 支持 jad → mc → redefine 三步完成热更新:先反编译出源码,修改后重新编译,再加载替换运行中的类。整个过程不重启 JVM,秒级生效。
详细步骤
第一步:反编译目标类
# 反编译并只输出源码
[arthas@12345]$ jad --source-only com.example.service.OrderService > /tmp/OrderService.java
第二步:修改源码
在服务器上编辑 /tmp/OrderService.java,修复 bug(如将 if (status == 1) 改为 if (status != 1))。
# 使用 vim 或 sed 修改
vim /tmp/OrderService.java
第三步:内存编译
# 编译修改后的 Java 文件
[arthas@12345]$ mc /tmp/OrderService.java -d /tmp/output
输出示例:
Memory compiler output:
/tmp/output/com/example/service/OrderService.class
第四步:热加载替换
# 加载新的 class 文件替换运行中的类
[arthas@12345]$ redefine /tmp/output/com/example/service/OrderService.class
输出 redefine success 即表示热更新成功,新的代码逻辑立即生效。
第五步:验证修复
# 用 watch 观察方法行为是否已变更
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params, returnObj}" -x 2
触发一次下单请求,确认返回结果正确。
注意事项与限制
| 限制项 | 说明 |
|---|---|
| 不能增删方法/字段 | redefine 只能修改方法体,不能改变类结构(方法签名、字段数量) |
| 不能修改继承关系 | 不能改变父类、接口列表 |
| 需要相同 ClassLoader | 多 ClassLoader 场景需指定 -c <classLoaderHash> |
| 不持久化 | JVM 重启后恢复原代码,仍需通过正式发布修复 |
| 需要 JDK | mc 命令依赖 JDK 的编译器,JRE 环境不可用 |
指定 ClassLoader 的热更新(Spring Boot 场景):
# 先查看类的 ClassLoaderHash
[arthas@12345]$ sc -d com.example.service.OrderService | grep classLoaderHash
# classLoaderHash 5a54a66
# 编译时指定 ClassLoader
[arthas@12345]$ mc -c 5a54a66 /tmp/OrderService.java -d /tmp/output
# redefine 时也指定
[arthas@12345]$ redefine -c 5a54a66 /tmp/output/com/example/service/OrderService.class
场景九:数据库连接池耗尽排查
问题现象
应用间歇性报 Cannot get a connection, pool error: Timeout waiting for idle object。大量请求堆积超时,但数据库本身负载正常。
排查思路
连接池耗尽通常有三种原因:一是连接泄漏(借出后未归还),二是慢 SQL 占用连接时间过长,三是连接池配置过小。需要查看连接池状态和连接使用情况。
详细步骤
第一步:查看连接池实例状态
# 查找连接池对象实例(以 HikariCP 为例)
[arthas@12345]$ vmtool --action getInstances --className com.zaxxer.hikari.HikariDataSource --limit 5
第二步:查看连接池内部状态
# 通过 OGNL 查看连接池的关键指标
[arthas@12345]$ ognl '#data = @com.zaxxer.hikari.HikariDataSource@class, \
#instances = #data.getName(), \
#pool = #data.getDeclaredField("pool"), \
#pool.setAccessible(true), \
#hikariPool = #pool.get(null), \
#hikariPool.toString()' -c 5a54a66
更实用的方式是直接查看连接池的统计信息:
# 查看连接池配置和当前状态
[arthas@12345]$ getstatic com.zaxxer.hikari.HikariPoolMXBean poolName
[arthas@12345]$ vmtool --action getInstances --className com.zaxxer.hikari.HikariPool --limit 1 \
--express 'instances[0]'
第三步:追踪连接获取耗时
# 追踪 getConnection 方法的耗时
[arthas@12345]$ trace com.zaxxer.hikari.HikariDataSource getConnection -n 5
如果 getConnection 耗时很长,说明连接池中确实没有可用连接。
第四步:排查慢 SQL 占用连接
# 观察数据库查询方法的执行时间
[arthas@12345]$ trace org.springframework.jdbc.core.JdbcTemplate query '#cost > 1000' -n 10
如果发现有 SQL 执行超过 1 秒,说明是慢 SQL 占用了连接。
第五步:排查连接泄漏
# 监控连接的借出和归还
[arthas@12345]$ watch com.zaxxer.hikari.pool.PoolEntry getConnection "{params, returnObj}" -x 1 -n 20
观察连接是否被正常归还。如果借出数量持续增长但归还数量不增长,说明存在泄漏。
第六步:查看线程持有哪些连接
# 查看哪些线程在等待连接
[arthas@12345]$ thread --state WAITING | grep -i "getConnection\|Hikari"
解决方案
| 根因 | 解决方案 |
|---|---|
| 连接泄漏 | 检查所有 getConnection 调用路径,确保在 finally 中归还;使用 try-with-resources |
| 慢 SQL | 优化 SQL、加索引;设置 SQL 超时时间 |
| 连接池过小 | 根据实际并发量调大 maximumPoolSize |
| 未设置超时 | 配置 connectionTimeout,避免无限等待 |
| 事务未提交/回滚 | 检查 @Transactional 注解的方法是否有异常未回滚的情况 |
场景十:特定用户数据问题排查
问题现象
某个特定用户(userId=12345)反馈下单异常,但其他用户正常。本地无法复现,因为本地没有该用户的数据环境。需要在生产环境中针对该用户的数据进行诊断。
排查思路
这是 Arthas 最典型的应用场景——在生产环境针对特定条件的方法调用进行观测。核心是利用 watch 的条件表达式,只在特定用户调用时输出观测数据。
详细步骤
第一步:条件观测方法入参
# 只在 userId=12345 的调用时输出入参和返回值
[arthas@12345]$ watch com.example.service.OrderService createOrder \
"{params, returnObj}" \
"params[0].userId == 12345" \
-x 3
让用户再操作一次,Arthas 会捕获该次调用的完整入参和返回值。
第二步:追踪该用户的调用链
# 条件追踪,只在特定用户调用时追踪耗时
[arthas@12345]$ trace com.example.service.OrderService createOrder \
'params[0].userId == 12345' \
-n 1
观察该用户的调用链中哪个环节出了问题。可能发现该用户的某个关联数据异常(如收货地址为空、优惠券已过期等)导致后续逻辑出错。
第三步:用 OGNL 直接查询该用户的数据
# 直接调用 Service 方法查询该用户数据(不影响业务,只读操作)
[arthas@12345]$ ognl '@com.example.SpringContextHolder@getBean(@com.example.service.UserDataService@class).getUserData(12345)' -x 3
或者在有 Spring 上下文的情况下:
# 通过 Spring ApplicationContext 获取 Bean 并调用方法
[arthas@12345]$ ognl '#context = @org.springframework.web.context.ContextLoader@getCurrentWebApplicationContext(), \
#service = #context.getBean("userService"), \
#service.getUserProfile(12345)' -x 3
第四步:用 tt 记录并回放调用
如果问题偶发且用户难以配合复现,可以先用 tt 持续记录方法调用,等问题出现后回放:
# 记录方法所有调用
[arthas@12345]$ tt -t com.example.service.OrderService createOrder
等一段时间后查看记录:
# 查看所有记录
[arthas@12345]$ tt -l
找到异常的那次调用(通过时间或返回值判断):
# 查看某次调用的详细信息
[arthas@12345]$ tt -i 1000
# 重放某次调用(重新执行该方法,使用记录时的入参)
[arthas@12345]$ tt -i 1000 -p
-p (play) 会用记录时的入参重新执行一次方法调用,可以在修复后验证逻辑是否正确。
tt 命令核心用法
tt -t <class> <method> 开始记录方法调用
tt -l 列出所有记录
tt -i <index> 查看指定记录详情
tt -i <index> -p 重放指定记录
tt --delete <index> 删除指定记录
tt --delete-all 清除所有记录
五、高级技巧
5.1 OGNL 表达式实战
OGNL(Object-Graph Navigation Language)是 Arthas 的表达式引擎,watch、trace、tt 等命令的条件表达式和观测表达式都基于 OGNL。掌握 OGNL 可以实现非常灵活的诊断逻辑。
常用 OGNL 技巧:
# 1. 调用静态方法
ognl '@java.lang.System@getProperty("java.version")'
# 2. 获取 Spring Bean 并调用方法
ognl '#context = @org.springframework.web.context.ContextLoader@getCurrentWebApplicationContext(), #bean = #context.getBean("myService"), #bean.doSomething()'
# 3. 在 watch 中构造复杂条件
watch com.example.service.OrderService createOrder "{params, returnObj}" \
"params[0].userId == 12345 && #cost > 500 && returnObj != null" -x 3
# 4. 在 watch 中调用方法处理参数
watch com.example.service.OrderService createOrder \
"@com.example.JsonUtils@toJson(params[0])" -x 1
# 5. 获取方法执行时间并做条件判断
watch com.example.service.OrderService createOrder \
"{params, #cost}" "#cost > 1000" -x 2
# 6. 获取目标对象的字段值
watch com.example.service.OrderService createOrder \
"target.cacheMap.size" -x 1
5.2 后台异步任务
当问题不确定何时出现,需要长时间监控某个方法时,可以将命令放到后台运行:
# 在后台执行 watch 命令,加 & 表示后台运行
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params, returnObj}" -x 2 &
# 查看后台任务
[arthas@12345]$ jobs
# 查看后台任务输出
[arthas@12345]$ jobs -d
# 停止后台任务
[arthas@12345]$ jobs -k <job_id>
配合日志保存功能,可以把后台任务的输出写入文件,事后分析:
# 启动时重定向输出到文件
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params}" -x 2 >> /tmp/arthas-watch.log &
5.3 profiler 生成火焰图
当需要做深度性能分析时,火焰图是最直观的工具:
# 开始采集(默认采样 CPU)
[arthas@12345]$ profiler start
# 等待一段时间(30秒~几分钟)
# 停止采集并生成火焰图
[arthas@12345]$ profiler stop --format html
生成的 HTML 火焰图可以直接在浏览器中打开查看。火焰图的横轴是采样数量(代表 CPU 占用),纵轴是调用栈深度。宽度越宽的方法,占用的 CPU 时间越多。
# 采集内存分配火焰图
[arthas@12345]$ profiler start --event alloc
# 采集指定时间后自动停止
[arthas@12345]$ profiler start --duration 60
5.4 批处理命令
需要一次性执行多条命令时,可以将命令写入文件,用 batch 模式执行:
# 创建命令文件
cat > /tmp/arthas-commands.txt << 'EOF'
dashboard -n 1
thread -n 5
sc -d com.example.service.OrderService
jad com.example.service.OrderService
EOF
# 批量执行
java -jar arthas-boot.jar 12345 -f /tmp/arthas-commands.txt
5.5 条件断点式排查
结合 watch 的条件表达式,可以实现类似 IDE 条件断点的效果:
# 模拟"条件断点":当订单金额大于 10000 且用户为 VIP 时才观测
watch com.example.service.OrderService createOrder \
"{params, returnObj, target}" \
"params[0].amount > 10000 && params[0].userLevel == 'VIP'" \
-x 4 -n 1
-n 1 表示只匹配一次就停止,相当于"命中一次断点"。
5.6 多 ClassLoader 场景处理
Spring Boot、OSGi、Tomcat 等环境下存在多个 ClassLoader,需要指定正确的 ClassLoader:
# 查找类时指定 ClassLoaderHash
[arthas@12345]$ sc -d com.example.service.OrderService
# 输出中找到 classLoaderHash: 5a54a66
# 所有命令都可以通过 -c 指定 ClassLoader
[arthas@12345]$ watch -c 5a54a66 com.example.service.OrderService createOrder "{params}" -x 2
[arthas@12345]$ jad -c 5a54a66 com.example.service.OrderService
[arthas@12345]$ redefine -c 5a54a66 /tmp/OrderService.class
六、生产环境注意事项
6.1 性能影响控制
Arthas 使用字节码增强技术,对目标方法织入观测逻辑,会带来一定的性能开销。生产环境使用时需要注意:
| 措施 | 说明 |
|---|---|
始终设置 -n 参数 |
限制匹配次数,避免无限期增强方法。-n 5 表示匹配 5 次后自动停止 |
| 避免对高频方法长时间监控 | QPS 上万的方法,watch/trace 持续运行会显著影响性能 |
| 排查完毕及时停止 | 用 stop 或 Ctrl+C 停止所有增强,恢复原始字节码 |
| 使用条件过滤 | 通过条件表达式减少观测输出量,降低序列化和 I/O 开销 |
| 避开流量高峰 | 尽量在低峰期执行重操作(如 heapdump、profiler) |
6.2 安全注意事项
| 风险点 | 防护建议 |
|---|---|
| Arthas 可执行任意代码 | 生产环境严格限制 Arthas 访问权限,不暴露 Web Console 到公网 |
| 连接端口安全 | 默认 telnet 3658 / HTTP 8563 端口不鉴权,需通过网络策略隔离 |
| 敏感数据泄露 | watch 可能输出包含用户隐私的参数,注意脱敏 |
| 热更新风险 | redefine 可能引入新 bug,建议先在测试环境验证 |
| 操作审计 | 启用 --stat-url 记录所有执行命令,便于审计追溯 |
6.3 最佳实践清单
- 先
dashboard后深入——先看全局指标,再针对性深入,避免盲目猜测。 trace优于watch做性能分析——trace展示调用链耗时分布,watch侧重数据观测。- 善用条件表达式——减少噪声,精准定位。
-n必加——任何观测命令都应设置匹配次数上限。- 用完即
stop——排查完毕后执行stop清理增强,避免长期影响。 - 热更新要谨慎——
redefine仅作紧急止血,正式修复仍需走发布流程。 - 保留操作记录——重要排查过程保存日志,便于复盘和知识沉淀。
- 建立团队命令库——将常用排查命令整理成脚本,新成员可快速上手。
6.4 常见问题与排错
| 问题 | 原因 | 解决方案 |
|---|---|---|
attach 失败 |
JDK 版本不匹配或权限不足 | 确认 Arthas 版本支持目标 JDK;用与应用相同的 JDK 启动 Arthas |
| 命令无输出 | 类未加载或 ClassLoader 不对 | 用 sc 确认类已加载;用 -c 指定正确的 ClassLoaderHash |
mc 编译失败 |
JDK 版本或缺少依赖 | 确保使用 JDK(非 JRE);检查源码中的 import 是否可用 |
redefine 报错 |
类结构变更或签名不匹配 | 确保只修改方法体,不增删方法/字段;使用相同 ClassLoader 编译 |
trace 输出不全 |
默认跳过 JDK 方法 | 加 --skipJDKMethod false 查看完整链路 |
watch 看不到内部字段 |
展开层级不够 | 增加 -x 参数值(如 -x 4) |
七、常用命令速查表
7.1 一分钟定位命令
# 看全局
dashboard
# 找最忙线程
thread -n 3
# 看线程堆栈
thread <thread_id>
# 反编译确认代码
jad <class> <method>
# 追踪方法耗时
trace <class> <method> -n 5
# 观测入参出参
watch <class> <method> "{params, returnObj}" -x 3
# 查看日志级别
logger
# 改日志级别
logger --name <package> --level DEBUG
# 查看内存
memory
# 查堆内对象
vmtool --action getInstances --className <class> --limit 10
# 查看静态字段
getstatic <class> <field>
# 查看已加载类
sc -d <class>
# 执行 OGNL
ognl '<expression>'
# 热更新
jad --source-only <class> > /tmp/X.java # 反编译
mc /tmp/X.java -d /tmp/output # 编译
redefine /tmp/output/<path>/X.class # 加载
# 火焰图
profiler start
profiler stop --format html
# 退出
quit # 退出会话
stop # 彻底卸载
7.2 命令参数速查
watch 内置变量:
| 变量 | 含义 |
|---|---|
params |
入参数组,params[0] 为第一个参数 |
returnObj |
返回值 |
throwExp |
抛出的异常对象 |
target |
方法所在对象实例 |
#cost |
方法执行耗时(毫秒) |
常用条件表达式示例:
#cost > 500 # 耗时超过 500ms
params[0].userId == 12345 # 特定用户
#throwExp != null # 抛出异常时
returnObj == null # 返回值为 null
params[0].userId == 12345 && #cost > 500 # 组合条件
trace/ watch 公共参数:
| 参数 | 含义 |
|---|---|
-n <N> |
匹配次数上限(强烈建议每次都加) |
-x <N> |
对象展开层级(watch 用) |
-c <hash> |
指定 ClassLoader |
-v |
打印条件表达式执行结果 |
#cost |
耗时(ms),用于条件过滤 |
7.3 场景到命令映射表
| 排查场景 | 首选命令 | 辅助命令 |
|---|---|---|
| CPU 飙高 | thread -n |
trace, jad, profiler |
| 接口慢 | trace |
watch, monitor |
| 方法异常 | watch |
stack, jad |
| 内存泄漏/OOM | memory, vmtool |
dashboard, heapdump |
| 日志不够 | logger |
ognl |
| 类冲突 | sc -d, jad |
dump, classloader |
| 死锁 | thread -b |
thread --state BLOCKED |
| 热修复 | jad → mc → redefine |
ognl |
| 连接池耗尽 | vmtool, trace |
watch, thread |
| 特定用户问题 | watch (条件) |
tt, ognl |
| 性能深度分析 | profiler |
trace, monitor |
| 查看方法调用统计 | monitor |
tt |
7.4 危险操作警告
以下操作在生产环境需格外谨慎,建议在低峰期执行并做好回滚准备:
| 操作 | 风险等级 | 风险说明 |
|---|---|---|
redefine |
高 | 热更新代码可能引入新 bug,且不持久化(重启后丢失) |
heapdump |
中 | 导出堆 dump 会暂停应用(STW),大堆可能暂停数秒 |
profiler start |
中 | 采样有性能开销,长时间运行会影响吞吐量 |
ognl 调用写方法 |
高 | 直接调用 Service 写方法会修改业务数据 |
thread 暂停线程 |
高 | 暂停业务线程可能导致请求超时 |
无 -n 的 watch/trace |
中 | 高频方法持续增强会显著影响性能 |
结语:Arthas 的核心价值在于"不重启、不改代码、实时观测"。熟练掌握
dashboard → thread → trace → watch → jad这条核心排查链路,配合条件表达式和 OGNL,可以覆盖 90% 以上的线上问题排查需求。建议在日常开发中多加练习,形成肌肉记忆,线上出问题时才能快速定位、从容应对。