Skip to content

Arthas 与线上问题诊断 ​

S6-011选修高级约 60 分钟JDK 21Java 高级与架构 · JVM

1. 本节定位 ​

「线上出问题不能重启、不能加日志、更不能停服调试」—— 这就是诊断工具存在的理由。

★ 本节先造一个「三种病集于一身」的程序,再用 JDK 自带工具与 Arthas 逐一定位(全部一手实测):

text
症状:CPU 被一个死循环线程占满 + 两个线程死锁 + 一个接口偶发 300ms 慢

Arthas  thread -n 3  → "cpu-burner" cpuUsage=95.54%(栈直指 DiagnoseTarget.java:27)✓✓
Arthas  thread -b    → deadlock-1 BLOCKED ... <---- but blocks 1 other threads! ✓✓✓
Arthas  trace        → slowQuery 里 doFetch 占 99.91% 耗时 ✓✓✓
JDK     jcmd Thread.print → Found one Java-level deadlock ✓✓(自带工具同样能抓)

2. 学习目标 ​

  • 会按「CPU 高 / 内存涨 / 接口慢」三条线选择诊断命令 ★★★;
  • 会用 thread、trace、watch、sc、jad 完成一次在线取证 ★★★;
  • 知道 Arthas 与 JDK 自带工具(jps/jcmd)的分工与配合 ★★★;
  • 知道诊断工具本身的代价与生产使用边界 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ 线上三件事最贵:复现、重启、加日志 ✗✗ —— 诊断工具让你不改代码、不重启就能看到现场 ✓✓✓:

只能靠猜 ✗工具给出的答案 ✓
「哪个线程把 CPU 吃满了」忙线程榜 + 调用栈(精确到行号)
「接口为什么慢」方法调用树 + 每层耗时占比
「线上跑的到底是哪个版本的类」反编译当前字节码 + 加载器信息

5. 核心概念 ​

5.1 三条线对三组命令 ★★★ ​

症状第一反应进阶
CPU 高 ✗✗thread -n 3(忙线程榜)thread <id> 看栈 → trace 定位热点方法
内存涨 / OOM ✗✗dashboard、jvm、memoryheapdump 导出堆(配合 《GC 日志分析与 JVM 调优》 的 HeapDumpOnOutOfMemoryError)
接口慢 / 偶发卡 ⚠️trace 类 方法(耗时占比)watch 看入参与返回、monitor 看成功率与 RT
死锁 / 线程卡住 ✗✗thread -b(阻塞关系)jcmd <pid> Thread.print(自带,含死锁检测)
类从哪来 / 版本对不对sc -d 类(加载器与来源)jad 类 反编译当前字节码

5.2 Arthas 的原理(一句话)★★★ ​

text
attach 到目标 JVM → 加载 Arthas Agent → 按需对类做字节码增强 → 命令输出结果

★★ 一切在线诊断工具(Arthas / APM / 火焰图)本质都是「Agent + 字节码增强」 ✓✓✓ —— 原理细节正是《字节码与 Java Agent》 要亲手实现的东西 ✓。

5.3 装与跑(Windows 实测路径)★★ ​

powershell
# ① 下载完整包(官方镜像,约 17MB):https://arthas.aliyun.com/download/4.3.5?mirror=aliyun
# ② 解压到 labs/java-deep/tools/arthas(本节实测路径;该目录不入库)

# 在 labs/java-deep 目录下执行:批处理模式,attach 后跑命令并退出(适合演练与脚本)
java -jar tools/arthas/arthas-boot.jar --arthas-home tools/arthas -c "jvm" <pid>

★ 注意 ✓:--arthas-home 指向解压目录,避免每次重新下载;-c 里的多条命令用 ; 分隔 ✓。

★ 常用命令速查 ✓:

text
dashboard        总览(线程/内存/GC,交互式)
jvm              运行时信息(本节实测里 DEADLOCK-COUNT=2 一眼可见 ✓✓)
thread -n 3      最忙的 3 个线程
thread -b        谁阻塞了别人(死锁首选)✓✓✓
trace 类 方法     调用树 + 各层耗时占比(-n 3 抓 3 次调用后自动退出)
watch 类 方法     观测参数 / 返回值 / #cost
sc -d 类          类信息(加载器链、classLoaderHash)
jad 类            反编译当前字节码
vmoption          查看/动态修改 JVM 参数
profiler          CPU 火焰图(Linux/Mac;Windows 不支持,实测报错 ✓)

5.4 与 JDK 自带工具的分工 ★★★ ​

工具定位适用
jps -l找到目标进程永远的第一步 ✓
jcmd多功能:Thread.print、GC.heap_info、JFR无依赖、最稳,先试它 ✓✓✓
jstack / jmap / jstat线程栈 / 堆快照 / GC 统计老三样,仍在用 ✓
Arthas动态增强与观测需要「看方法内部」时才上 ✓✓

★★ 顺序建议:jps 找到进程 → jcmd 免费拿基础信息 → 需要更细才上 Arthas ✓✓✓。

5.5 生产使用边界 ★★★ ​

  • 诊断工具是「临时手术刀」,不是常驻监控 ✗✓:用完 stop 卸载;
  • 增强有开销:trace 高流量方法会明显放大耗时(本节 watch 抓 3 次就退,就是控制影响)✓;
  • 需要授权与审计:attach 能看任何对象、改参数、反编译代码 ✗✗;
  • JDK 25 起动态加载 agent 默认会逐步收紧(实测已出现警告 "Dynamic loading of agents will be disallowed by default in a future release")⚠️ —— 未来生产需要显式开启 -XX:+EnableDynamicAgentLoading ✓✓。

6. 递进示例 ​

6.1 造病:三种典型故障 ​

java
startCpuBurner();   // 死循环线程:while(true) x += Math.sqrt(x + 1)
startDeadlock();    // 两线程交叉加锁:A 等 B、B 等 A
startSlowCalls();   // 慢调用:slowQuery → normalize → doFetch(sleep 200~400ms)

6.2 定位:CPU 高 ​

text
[arthas@28972]$ thread -n 3
"cpu-burner" Id=35 cpuUsage=95.54% deltaTime=203ms time=115406ms RUNNABLE
    at app//cn.kb.learn.deep.jvm.DiagnoseTarget.lambda$startCpuBurner$0(DiagnoseTarget.java:27)

★ 一行到行号 ✓✓✓:95.54% + DiagnoseTarget.java:27 = 证据链闭环(不用猜、不用加日志)。

6.3 定位:死锁 ​

text
[arthas@28972]$ thread -b
"deadlock-1" Id=36 BLOCKED on java.lang.Object@1c9b2d30 owned by "deadlock-2" Id=37
    at app//cn.kb.learn.deep.jvm.DiagnoseTarget.lambda$startDeadlock$0(DiagnoseTarget.java:40)
    -  blocked on java.lang.Object@1c9b2d30
    -  locked java.lang.Object@610234e7 <---- but blocks 1 other threads!

★ 自带工具同样能抓 ✓✓(交叉验证,互不替代):

text
$ jcmd 28972 Thread.print
Found one Java-level deadlock:
=============================
"deadlock-1": waiting to lock monitor … which is held by "deadlock-2"
"deadlock-2": waiting to lock monitor … which is held by "deadlock-1"

6.4 定位:慢接口 ​

text
[arthas@28972]$ trace cn.kb.learn.deep.jvm.DiagnoseTarget slowQuery -n 3
`---[295.0808ms] cn.kb.learn.deep.jvm.DiagnoseTarget:slowQuery()
    +---[0.01% 0.0262ms ] cn.kb.learn.deep.jvm.DiagnoseTarget:normalize() #60
    `---[99.91% 294.8173ms ] cn.kb.learn.deep.jvm.DiagnoseTarget:doFetch() #61

★★ 耗时占比直接把「该优化哪里」钉死了 ✓✓✓:normalize 0.01%、doFetch 99.91%。

6.5 看参数与返回 ​

text
[arthas@28972]$ watch cn.kb.learn.deep.jvm.DiagnoseTarget slowQuery '{params, returnObj, #cost}' -n 3 -x 2
ts=…; [cost=300.1667ms] result=@ArrayList[
    @Object[][ @String[ID-124] ],      ← 入参
    @String[result-ID-124],            ← 返回值
    @Double[300.1667],                 ← 本次耗时
]

6.6 追「这个类是谁加载的、跑的是哪版代码」 ​

text
[arthas@28972]$ sc -d cn.kb.learn.deep.jvm.DiagnoseTarget
 class-loader      +-jdk.internal.loader.ClassLoaders$AppClassLoader@105be200
                     +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@34d4999
 classLoaderHash   105be200

★ sc -d + jad 组合 = 「线上版本取证」 ✓✓(呼应 《类加载机制》 加载器链); jad 会把当前字节码反编译出来(本机实测 CFR 反编译,带行号)✓。

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/DiagnoseTarget.java(故意有病的常驻程序)

★ 「有病」程序实测输出(节选) ✓:

text
DiagnoseTarget 启动,pid=28972
[heartbeat] 第 1 次,进程 28972 存活
[slow-caller] 第 1 次调用返回 result-ID-1
…

★ jvm 命令实测(节选) ✓✓:

text
 THREAD
 COUNT                  22
 DAEMON-COUNT           21
 DEADLOCK-COUNT         2        ← 死锁被一眼点名 ✓✓✓
 MEMORY
 HEAP-MEMORY-USAGE      used : 13781688(13.1 MiB)  max : 8510242816(7.9 GiB)
 GARBAGE-COLLECTORS     G1 Young Generation count 3 / time 34ms
 CLASS-LOADING          LOADED-CLASS-COUNT 5740

★ JDK 自带工具实测 ✓:

text
$ jps -l
28972 cn.kb.learn.deep.jvm.DiagnoseTarget        ← 目标进程
42220 C:\...\project-a-1.0.0.jar                 ← 顺手看到别的服务
14204 ...redhat.java-…-launcher…jar              ← IDE 的语言服务器

$ jcmd 28972 GC.heap_info
garbage-first heap   total reserved 8310784K, committed 69632K, used 31835K
 region size 4096K, 6 young (24576K), 2 survivors (8192K)

★ 一个环境边界(实测报错,如实记录) ⚠️:

text
[arthas@28972]$ profiler start -d 5 -f flamegraph.html
AsyncProfiler error: Current OS do not support AsyncProfiler, Only support Linux/Mac.

Windows 下火焰图不可用 —— 这本身就是一条选型信息:复杂性能分析在 Linux 服务器上做 ✓✓。

8. Debug 观察点 ​

  1. 把 thread -n 3 改成 thread -n 1:确认第一名就是 cpu-burner ✓✓;
  2. thread -b 之外再试 thread --state BLOCKED:列出所有阻塞线程 ✓✓;
  3. 对 slowQuery 做 monitor -c 5:看成功率与平均 RT(交互式,Ctrl+C 结束)✓;
  4. vmoption PrintGC 动态开 GC 日志:不改启动参数也能加观测 ✓✓(呼应 《GC 日志分析与 JVM 调优》);
  5. 诊断结束后 stop:确认 Arthas 从目标 JVM 卸载 ✓✓✓。

9. 常见错误 ​

#错误说明
1CPU 只看 top,不看线程栈 ✗✗栈 + 行号才是证据链 ✓✓
2把 Arthas 当长期监控装在生产 ✗✗它是临时诊断工具,用完 stop ✓✓
3对超高流量方法挂 trace 不管 ✗✗有开销,务必用 -n 限额 ✓✓
4死锁只重启不取证 ✗✗重启前先 thread -b / jcmd Thread.print 留证据 ✓✓
5忽略「动态 agent 将默认禁止」的警告 ⚠️新 JDK 版本要显式开启 ✓
6Windows 上折腾火焰图 ✗换 Linux,或改用 JFR ✓

10. 动手实验 ​

  1. 启动 DiagnoseTarget,用 jps 找到它 ✓✓;
  2. 依次跑通本节全部命令:thread -n 3、thread -b、trace -n 3、watch -n 3、sc -d、jad ✓✓✓;
  3. 用 jcmd Thread.print 交叉验证死锁 ✓✓;
  4. 把 DiagnoseTarget 的慢方法改成「先注释掉 sleep」再 trace:验证耗时占比随之变化 ✓✓;
  5. 诊断完 stop 卸载 Arthas,再用 jps 确认 ✓✓✓。

验收方式:给一个「CPU 高 + 接口慢」的现场,能说出先跑哪三条命令、每条命令看什么、结论怎么下。

扩展任务:给你自己的项目 A 写一份《线上诊断手册》:三条线各一个命令清单 + 授权流程 + 取证留档要求 ✓✓

11. 练习题 ​

  1. 「CPU 高」「接口慢」「死锁」三组症状各自第一命令是什么?
  2. Arthas 的实现原理一句话说完?
  3. sc -d 输出里的 classLoaderHash 有什么用?
  4. 为什么 trace 要加 -n?
  5. 本轮实测中 profiler 在 Windows 上报了什么?说明什么?
参考答案(要点)
  1. thread -n 3 / trace 类 方法 / thread -b ✓✓★
  2. attach 目标 JVM + 加载 agent + 字节码增强(《字节码与 Java Agent》 里亲手实现)✓✓★
  3. 定位类的加载器(排查同名类冲突、版本不一致)✓★
  4. 限制增强次数与开销(抓 3 次自动退出)✓✓★
  5. 「Current OS do not support AsyncProfiler」:火焰图能力只在 Linux/Mac,Windows 需换 JFR 或上服务器 ✓★

12. 掌握标准 ​

  • [ ] 三条线各会第一命令 ★★★;
  • [ ] 能用 trace/watch 做一次在线取证 ★★★;
  • [ ] 会 sc/jad 做版本核对 ★★★;
  • [ ] 知道 jcmd 与 Arthas 的分工 ★★★;
  • [ ] 知道诊断工具的开销与边界 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《字节码与 Java Agent》 字节码与 Java Agent —— 亲手实现「Arthas 背后的那套机制」。 Linux 服务端的同类排查思路见 《进程与端口排查》(进程与端口排查)。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引