Java 线上排查工具链:从 top 到 Arthas
一句话结论(30s)
线上排查的关键是按”现象 → 指标 → 代码”逐层下钻——因为 CPU 飙高、内存 OOM、接口慢、死锁各有对应的工具链(top/jstack、dump/MAT、Arthas trace/watch、jstack 死锁检测),用对工具才能从”服务器指标异常”一路定位到”具体线程或方法”。
核心原理(2min)
- CPU 飙高:
top -c找进程 →top -H -p找线程 → TID 转十六进制 →jstack匹配线程栈。 - 内存 OOM:
-XX:+HeapDumpOnOutOfMemoryError自动 dump → MAT 的 Leak Suspects + Dominator Tree 找大对象。 - 接口慢:Arthas
trace打印调用链每步耗时,watch看入参 / 返回值 / 异常,无需加日志重启。 - 死锁:
jstack自动检测 “Found one Java-level deadlock”。 - 热更新:
redefine底层调 JVMTIredefineClasses(),只能改方法体、不能增删字段/方法,重启后失效。
底层深入(5-10min)
场景一:CPU 飙高
1. top -c → 找到高 CPU 的 Java 进程 PID
2. top -H -p <pid> → 找到该进程中最高 CPU 的线程 TID
3. printf '%x\n' <tid> → 将 TID 转十六进制 0xtid
4. jstack <pid> | grep 0xtid -A 20 → 看这个线程正在执行什么代码
通常能定位到:死循环中的某个方法、反复 Full GC 的 GC 线程、或等待锁的热点代码。
思考穿插:为什么排查 CPU 飙高要先”进程 → 线程 → 十六进制 TID → jstack”这么绕一圈,而不是直接看日志?因为日志是事后记录、且高 CPU 的死循环往往不打日志——你只能靠”实时抓那个正在烧 CPU 的线程”来定位。这一步的关键是理解 Linux 线程 TID 和 jstack 里十六进制 nid 的对应关系,否则拿到了线程号也匹配不上代码。
场景二:内存 OOM
1. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
→ OOM 时自动 dump
2. MAT 打开 dump 文件 → Leak Suspects → 自动找最大保留对象
3. Dominator Tree → 看你怀疑的大对象的引用链
MAT 的”支配树”直观展示每个对象”统治”了多少内存。一个 HashMap 持有百万个 Entry → 支配树中 HashMap 的”保留大小”就是几 GB。
场景三:接口慢/无响应
Arthas trace 命令追踪方法调用链的每步耗时:
trace com.example.ShopService getShopDetail -n 5
# 输出:
# `---[150.3ms] ShopService:getShopDetail()
# +---[80.2ms] getShop() ← MySQL 查询最慢
# +---[5.1ms] getScore() ← Redis 正常
# +---[30.0ms] getHotBlogs()
# `---[35.0ms] searchNearby()
精确知道哪一步耗时最多,无需加日志重启。
思考穿插:为什么 Arthas
trace能”不重启、不加日志”就拿到方法级耗时?因为它基于字节码增强,在运行期动态往目标方法里插入计时逻辑——这正好弥补了传统排查的两大痛点:加日志要重新发布、重新发布还可能破坏现场。理解”字节码增强”这个原理,你就明白它为什么能无侵入地做到这一切。
Arthas watch 看入参、返回值、异常:
watch com.example.OrderService createOrder '{params, returnObj, throwExp}' -x 3
场景四:死锁
jstack <pid> | grep -A 50 "Found one Java-level deadlock"
jstack 自动检测死锁——线程 A 持有锁 X 等锁 Y,线程 B 持有锁 Y 等锁 X → jstack 直接告诉你”找到了 Java 级死锁”。
Arthas redefine 热更新
redefine /path/to/UpdatedClass.class
底层调 JVMTI redefineClasses()——JVM 用新字节码替换方法体,JIT 缓存失效重新编译。限制:不能新增/删除字段或方法(只能改方法体),不能改继承层次。运行中的方法实例不受影响(当前栈帧继续执行旧方法直到返回)。
适合临时 debug / 紧急逻辑修复。真正的 hotfix 仍需走正式发布,因为 redefine 重启后失效。
思考穿插:为什么
redefine只能改方法体、不能增删字段或方法?因为 JVMTI 的redefineClasses本质是”用新字节码替换已有方法体”,但类的结构(字段、方法签名、继承关系)已被加载进 JVM、关系到对象内存布局,运行时无法安全改动——这是 JVM 类加载的硬约束,不是 Arthas 偷懒。
总结
| 问题 | 命令 | 关键工具 |
|---|---|---|
| CPU 高 | top → jstack | 线程栈分析 |
| 内存高 | jmap -dump → MAT | 支配树 + Leak Suspects |
| 接口慢 | Arthas trace | 方法级耗时分布 |
| 死锁 | jstack | 自动检测 Found deadlock |
| 热修复 | Arthas redefine | JVMTI redefineClasses |
章末提问
线上排查是最值得深挖的场景题,对方通常会顺着”为什么用这个工具”往下追。以下是三个典型追问:
-
“CPU 飙高,你的完整排查步骤是什么?” 结论先行:「
top定位进程 →top -H -p定位线程 → TID 转十六进制 →jstack匹配线程栈,找到正在执行的代码。」因为 对方要看你会不会”逐层下钻”——从服务器指标到具体线程再到代码行,这套链路能证明你有真实排查经验,而不是只会背命令。 -
“
jstack和 Arthastrace分别适合什么场景?” 结论先行:「jstack看线程状态、死锁、阻塞等”瞬间快照”;trace看方法调用链每一步的耗时分布,适合接口慢的定位。」因为 对方在考你的”工具选择判断力”——能用对工具说明你理解每个工具的定位边界,而不是把jstack当万能钥匙。 -
“Arthas
redefine热更新为什么不能替代正式发布?” 结论先行:「因为底层 JVMTI 只允许改方法体、不能增删字段/方法,且重启后失效——它只适合临时止血,正式修复仍要走发布流程。」因为 这题在验证你理解”热更新的能力边界”——能说出 JVMTI 的限制和”重启失效”,证明你不是把 redefine 当成随意改代码的工具。