Skip to content

Arthas 实战案例

从问题到解决:Arthas 实战场

前面介绍了 Arthas 的各种命令,这一节通过真实案例,展示如何用 Arthas 组合拳解决线上问题。

案例一:接口响应时间暴增

问题描述

监控系统报警:用户查询接口 P99 延迟从 50ms 暴增到 2000ms。

诊断过程

bash
# 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
  ...

继续排查

bash
# 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

定位问题

bash
# 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)

发现原因

定位到是数据库连接池耗尽导致线程阻塞。进一步确认:

bash
# 6. 查看连接池相关代码
jad com.example.ResourcePool

# 发现:连接池 maxSize=10,但某查询耗时很长占用连接

解决方案

临时解决方案:用 ognl 临时扩大连接池

bash
# 临时扩大连接池
ognl '#pool = @com.example.ResourcePool@getInstance(); #pool.maxSize = 50; #pool.init()'

# 验证
monitor -c 3 com.example.Service fetchData

永久解决方案:优化慢查询,增加连接池大小。

案例二:CPU 持续 100%

问题描述

服务器 CPU 持续 100%,应用响应超时。

诊断过程

bash
# 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)

追踪热点代码

bash
# 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

定位问题代码

bash
# 5. 查看具体方法的调用
trace com.example.CacheService getAll '#cost > 0' -n 20

# 发现某个循环中频繁调用 hashCode
# 进一步用 jad 查看代码
jad com.example.CacheService

发现原因

找到问题:在循环中用 String 作为 HashMap 的 key,且每次循环都 new String() 创建新对象,导致 hashCode 每次都要重新计算。

解决方案

临时方案:

bash
# 使用 ognl 临时禁用问题缓存
ognl '@com.example.CacheService@setEnabled(false)'

# 或者:重启应用

永久方案:修复代码,将 new String(key) 改为直接使用 key

案例三:内存泄漏排查

问题描述

应用运行 3 天后,堆内存从 1GB 增长到 4GB(达到上限),触发频繁 Full GC。

诊断过程

bash
# 1. 查看内存趋势
dashboard

# 观察到:
# - Old Gen 使用率持续上升
# - Full GC 后内存不下降
# - 疑似内存泄漏

生成堆转储

bash
# 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

分析堆转储

bash
# 下载到本地用 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 作为缓存,没有清理机制,不断增长。

bash
# 6. 验证:用 Arthas 查看缓存大小
ognl '@com.example.Cache@SIZE'

# 输出:1000000

解决方案

临时方案:清空缓存

bash
# 方案一:使用 ognl 清空
ognl '@com.example.Cache@clear()'

# 方案二:重新加载(如果问题是类加载器持有引用)
# 需要重启或重新加载类

永久方案:为缓存添加 TTL 或大小限制,使用成熟的缓存框架(如 Caffeine、Guava Cache)。

案例四:死锁导致服务假死

问题描述

服务突然无响应,但 CPU 和内存都正常。重启后暂时恢复,但几小时后又出现。

诊断过程

bash
# 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 状态

检测死锁

bash
# 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)

查看死锁详情

bash
# 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

发现原因

两个线程以不同的顺序获取两把锁,形成循环等待:

java
// 线程 1
lockA.lock();
lockB.lock();  // 等待 LockB

// 线程 2
lockB.lock();
lockA.lock();  // 等待 LockA

// → 死锁!

解决方案

临时方案:重启应用(线程无法在线程层面中断)

bash
# 如果可以,用 jstack 保存现场后重启
jstack > /tmp/deadlock.txt

永久方案:

  1. 统一锁的获取顺序(都先 A 后 B,或都先 B 后 A)
  2. 使用 tryLock() 替代 lock()
  3. 减少锁的粒度
  4. 使用并发工具类(ConcurrentHashMapStampedLock 等)替代锁

案例五:方法调用异常,但日志没有记录

问题描述

部分用户反馈接口返回异常数据,但服务端日志没有相关记录。

诊断过程

bash
# 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]

追踪调用链路

bash
# 4. 用 trace 查看调用链路
trace com.example.Service process -n 5

# 5. 用 tt 记录多次调用
tt -t com.example.Service process
# 触发问题调用
tt -l

发现原因

tt -i 1000 查看历史记录,发现某次调用传入了 null 参数。

解决方案

  1. 在入口添加参数校验
  2. 修复调用方传入 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)

基于 VitePress 构建