Arthas 实战案例
从问题到解决:Arthas 实战场
前面介绍了 Arthas 的各种命令,这一节通过真实案例,展示如何用 Arthas 组合拳解决线上问题。
案例一:接口响应时间暴增
问题描述
监控系统报警:用户查询接口 P99 延迟从 50ms 暴增到 2000ms。
诊断过程
# 1. 连接 Arthas
java -jar arthas-boot.jar
# 选择目标进程
# 2. 查看整体状态
dashboard
# 观察到:
# - CPU 正常(10% 左右)
# - 堆内存正常
# - 有一个线程 CPU 占用 80%,且在 WAITING 状态dashboard 关键输出:
Threads:
pool-1-thread-3 80% WAITING time=5m
pool-1-thread-4 5% WAITING time=3m
...继续排查
# 3. 查看最耗 CPU 的线程
thread -n 5
# 4. 查看阻塞的线程
thread -b
# 输出:
# Blocked threads:
# pool-1-thread-3 waiting time: 300000ms
# at com.example.ResourcePool.get(ResourcePool.java:45)
# ← waiting to lock <0x000000076cf8bdf0>
# held by: pool-1-thread-1定位问题
# 5. 查看具体线程的堆栈
thread 3
# 输出:
# "pool-1-thread-3" Id=23 WAITING
# at com.example.ResourcePool.get(ResourcePool.java:45)
# at com.example.Service.fetchData(Service.java:78)
# at com.example.Controller.getData(Controller.java:32)发现原因
定位到是数据库连接池耗尽导致线程阻塞。进一步确认:
# 6. 查看连接池相关代码
jad com.example.ResourcePool
# 发现:连接池 maxSize=10,但某查询耗时很长占用连接解决方案
临时解决方案:用 ognl 临时扩大连接池
# 临时扩大连接池
ognl '#pool = @com.example.ResourcePool@getInstance(); #pool.maxSize = 50; #pool.init()'
# 验证
monitor -c 3 com.example.Service fetchData永久解决方案:优化慢查询,增加连接池大小。
案例二:CPU 持续 100%
问题描述
服务器 CPU 持续 100%,应用响应超时。
诊断过程
# 1. 查看 CPU 占用最高的线程
top -Hp <pid>
# 找到 CPU 占用最高的 Java 线程 ID
# 2. 在 Arthas 中查看线程
thread -n 3
# 输出:
# "pool-1-thread-1" cpu=85% RUNNABLE
# at java.lang.String.hashCode(String.java:456)
# at java.util.HashMap.hash(HashMap.java:567)
# at java.util.HashMap.get(HashMap.java:678)追踪热点代码
# 3. 用 trace 追踪 hashCode 调用
trace com.example.* hashCode '#cost > 10' -n 5
# 4. 用 profiler 生成火焰图
profiler start --event cpu
# 等待一段时间
profiler stop --format html > /tmp/result.html
# 下载并分析 result.html定位问题代码
# 5. 查看具体方法的调用
trace com.example.CacheService getAll '#cost > 0' -n 20
# 发现某个循环中频繁调用 hashCode
# 进一步用 jad 查看代码
jad com.example.CacheService发现原因
找到问题:在循环中用 String 作为 HashMap 的 key,且每次循环都 new String() 创建新对象,导致 hashCode 每次都要重新计算。
解决方案
临时方案:
# 使用 ognl 临时禁用问题缓存
ognl '@com.example.CacheService@setEnabled(false)'
# 或者:重启应用永久方案:修复代码,将 new String(key) 改为直接使用 key。
案例三:内存泄漏排查
问题描述
应用运行 3 天后,堆内存从 1GB 增长到 4GB(达到上限),触发频繁 Full GC。
诊断过程
# 1. 查看内存趋势
dashboard
# 观察到:
# - Old Gen 使用率持续上升
# - Full GC 后内存不下降
# - 疑似内存泄漏生成堆转储
# 2. 生成堆转储(存活对象)
heapdump --live /tmp/heap.hprof
# 3. 查看对象分布
jvm | grep "heap"
# 4. 用 Arthas 观察创建对象最多的地方
watch com.example.* new '{params, returnObj}' -b
# 这个命令开销很大,慎用!
# 可以缩小范围
watch com.example.Cache put '{params, returnObj}' -b分析堆转储
# 下载到本地用 MAT 分析
# 或者用 Arthas 的 OQL
# Arthas 中使用 OQL 需要安装相关插件
# 5. 查看线程相关对象
thread -n 10
# 观察是否有线程持有大量对象定位泄漏源
用 MAT 分析堆转储后发现:
Dominator Tree:
com.example.Cache (Retained: 3GB)
└─ java.util.HashMap
└─ Entry[1000000] (1GB)
└─ char[100] × 1M发现原因
找到一个静态 HashMap 作为缓存,没有清理机制,不断增长。
# 6. 验证:用 Arthas 查看缓存大小
ognl '@com.example.Cache@SIZE'
# 输出:1000000解决方案
临时方案:清空缓存
# 方案一:使用 ognl 清空
ognl '@com.example.Cache@clear()'
# 方案二:重新加载(如果问题是类加载器持有引用)
# 需要重启或重新加载类永久方案:为缓存添加 TTL 或大小限制,使用成熟的缓存框架(如 Caffeine、Guava Cache)。
案例四:死锁导致服务假死
问题描述
服务突然无响应,但 CPU 和内存都正常。重启后暂时恢复,但几小时后又出现。
诊断过程
# 1. 连接 Arthas
# 2. 查看线程状态
thread -n 20
# 输出:
# "pool-1-thread-1" BLOCKED time=3600000ms
# "pool-1-thread-2" BLOCKED time=3600000ms
# "pool-1-thread-3" BLOCKED time=3600000ms
# 大量线程处于 BLOCKED 状态检测死锁
# 3. 检测死锁
thread -b
# 输出:
# Found 1 deadlock!
# ===
# "pool-1-thread-1" id=23 waiting for lock
# held by "pool-1-thread-2" (id=24)
# "pool-1-thread-2" id=24 waiting for lock
# held by "pool-1-thread-1" (id=23)查看死锁详情
# 4. 查看两个线程的堆栈
thread 23
thread 24
# 输出:
# Thread-1:
# at com.example.LockA.acquire()
# ← waiting to lock 0x000000076cf8bdf0
# ← locked by 0x000000076cf8bde0
#
# Thread-2:
# at com.example.LockB.acquire()
# ← waiting to lock 0x000000076cf8bde0
# ← locked by 0x000000076cf8bdf0发现原因
两个线程以不同的顺序获取两把锁,形成循环等待:
// 线程 1
lockA.lock();
lockB.lock(); // 等待 LockB
// 线程 2
lockB.lock();
lockA.lock(); // 等待 LockA
// → 死锁!解决方案
临时方案:重启应用(线程无法在线程层面中断)
# 如果可以,用 jstack 保存现场后重启
jstack > /tmp/deadlock.txt永久方案:
- 统一锁的获取顺序(都先 A 后 B,或都先 B 后 A)
- 使用
tryLock()替代lock() - 减少锁的粒度
- 使用并发工具类(
ConcurrentHashMap、StampedLock等)替代锁
案例五:方法调用异常,但日志没有记录
问题描述
部分用户反馈接口返回异常数据,但服务端日志没有相关记录。
诊断过程
# 1. 用 watch 监控方法
watch com.example.Service process '{params, returnObj, throwExp}' -e
# 2. 触发问题调用
curl http://localhost:8080/api/problem
# 3. Arthas 捕获到异常
# 输出:
# ts=2026-03-22 10:00:00 method=com.example.Service:process
# throwExp=java.lang.NullPointerException: Cannot invoke method on null
# params=[null]追踪调用链路
# 4. 用 trace 查看调用链路
trace com.example.Service process -n 5
# 5. 用 tt 记录多次调用
tt -t com.example.Service process
# 触发问题调用
tt -l发现原因
用 tt -i 1000 查看历史记录,发现某次调用传入了 null 参数。
解决方案
- 在入口添加参数校验
- 修复调用方传入 null 的问题
诊断 Checklist
遇到问题时,按以下顺序使用 Arthas:
□ 1. dashboard → 全局概览(CPU/内存/线程)
□ 2. thread -n → 最耗 CPU 的线程
□ 3. thread -b → 阻塞和死锁
□ 4. trace → 方法调用耗时
□ 5. watch → 方法入参和返回值
□ 6. jad → 查看当前运行的代码
□ 7. heapdump → 生成堆转储(MAT 分析)
□ 8. ognl → 动态修改配置或调用方法
□ 9. redefine → 热修复代码本节小结
Arthas 实战案例要点:
| 场景 | 症状 | Arthas 组合 |
|---|---|---|
| 接口慢 | 响应时间长 | dashboard → thread -b → trace |
| CPU 高 | CPU 100% | thread -n → profiler → jad |
| 内存泄漏 | 堆持续增长 | dashboard → heapdump → MAT |
| 死锁 | 服务假死 | thread -b → thread |
| 返回异常 | 日志缺失 | watch -e → tt |
| 配置错误 | 需要改配置 | ognl 临时修改 |
| 代码 BUG | 需要修复 | jad → mc → redefine |
Arthas 的精髓在于组合使用——先用 dashboard 定位方向,再用具体命令深入分析。熟练掌握后,生产环境 80% 的问题都能在不重启、不发版的情况下解决。
下一节,我们来看 JVM 参数类型(标准/-X/-XX)。
