主题
界面
主题
「线上出问题不能重启、不能加日志、更不能停服调试」—— 这就是诊断工具存在的理由。
★ 本节先造一个「三种病集于一身」的程序,再用 JDK 自带工具与 Arthas 逐一定位(全部一手实测):
症状: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 ✓✓(自带工具同样能抓)thread、trace、watch、sc、jad 完成一次在线取证 ★★★;★ 线上三件事最贵:复现、重启、加日志 ✗✗ —— 诊断工具让你不改代码、不重启就能看到现场 ✓✓✓:
| 只能靠猜 ✗ | 工具给出的答案 ✓ |
|---|---|
| 「哪个线程把 CPU 吃满了」 | 忙线程榜 + 调用栈(精确到行号) |
| 「接口为什么慢」 | 方法调用树 + 每层耗时占比 |
| 「线上跑的到底是哪个版本的类」 | 反编译当前字节码 + 加载器信息 |
| 症状 | 第一反应 | 进阶 |
|---|---|---|
| CPU 高 ✗✗ | thread -n 3(忙线程榜) | thread <id> 看栈 → trace 定位热点方法 |
| 内存涨 / OOM ✗✗ | dashboard、jvm、memory | heapdump 导出堆(配合 《GC 日志分析与 JVM 调优》 的 HeapDumpOnOutOfMemoryError) |
| 接口慢 / 偶发卡 ⚠️ | trace 类 方法(耗时占比) | watch 看入参与返回、monitor 看成功率与 RT |
| 死锁 / 线程卡住 ✗✗ | thread -b(阻塞关系) | jcmd <pid> Thread.print(自带,含死锁检测) |
| 类从哪来 / 版本对不对 | sc -d 类(加载器与来源) | jad 类 反编译当前字节码 |
attach 到目标 JVM → 加载 Arthas Agent → 按需对类做字节码增强 → 命令输出结果★★ 一切在线诊断工具(Arthas / APM / 火焰图)本质都是「Agent + 字节码增强」 ✓✓✓ —— 原理细节正是《字节码与 Java Agent》 要亲手实现的东西 ✓。
# ① 下载完整包(官方镜像,约 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 里的多条命令用 ; 分隔 ✓。
★ 常用命令速查 ✓:
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 不支持,实测报错 ✓)| 工具 | 定位 | 适用 |
|---|---|---|
| jps -l | 找到目标进程 | 永远的第一步 ✓ |
| jcmd | 多功能:Thread.print、GC.heap_info、JFR | 无依赖、最稳,先试它 ✓✓✓ |
| jstack / jmap / jstat | 线程栈 / 堆快照 / GC 统计 | 老三样,仍在用 ✓ |
| Arthas | 动态增强与观测 | 需要「看方法内部」时才上 ✓✓ |
★★ 顺序建议:jps 找到进程 → jcmd 免费拿基础信息 → 需要更细才上 Arthas ✓✓✓。
stop 卸载;trace 高流量方法会明显放大耗时(本节 watch 抓 3 次就退,就是控制影响)✓;-XX:+EnableDynamicAgentLoading ✓✓。startCpuBurner(); // 死循环线程:while(true) x += Math.sqrt(x + 1)
startDeadlock(); // 两线程交叉加锁:A 等 B、B 等 A
startSlowCalls(); // 慢调用:slowQuery → normalize → doFetch(sleep 200~400ms)[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 = 证据链闭环(不用猜、不用加日志)。
[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!★ 自带工具同样能抓 ✓✓(交叉验证,互不替代):
$ 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"[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%。
[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], ← 本次耗时
][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 反编译,带行号)✓。
代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/DiagnoseTarget.java(故意有病的常驻程序)
★ 「有病」程序实测输出(节选) ✓:
DiagnoseTarget 启动,pid=28972
[heartbeat] 第 1 次,进程 28972 存活
[slow-caller] 第 1 次调用返回 result-ID-1
…★ jvm 命令实测(节选) ✓✓:
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 自带工具实测 ✓:
$ 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)★ 一个环境边界(实测报错,如实记录) ⚠️:
[arthas@28972]$ profiler start -d 5 -f flamegraph.html
AsyncProfiler error: Current OS do not support AsyncProfiler, Only support Linux/Mac.Windows 下火焰图不可用 —— 这本身就是一条选型信息:复杂性能分析在 Linux 服务器上做 ✓✓。
thread -n 3 改成 thread -n 1:确认第一名就是 cpu-burner ✓✓;thread -b 之外再试 thread --state BLOCKED:列出所有阻塞线程 ✓✓;slowQuery 做 monitor -c 5:看成功率与平均 RT(交互式,Ctrl+C 结束)✓;vmoption PrintGC 动态开 GC 日志:不改启动参数也能加观测 ✓✓(呼应 《GC 日志分析与 JVM 调优》);stop:确认 Arthas 从目标 JVM 卸载 ✓✓✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | CPU 只看 top,不看线程栈 ✗✗ | 栈 + 行号才是证据链 ✓✓ |
| 2 | 把 Arthas 当长期监控装在生产 ✗✗ | 它是临时诊断工具,用完 stop ✓✓ |
| 3 | 对超高流量方法挂 trace 不管 ✗✗ | 有开销,务必用 -n 限额 ✓✓ |
| 4 | 死锁只重启不取证 ✗✗ | 重启前先 thread -b / jcmd Thread.print 留证据 ✓✓ |
| 5 | 忽略「动态 agent 将默认禁止」的警告 ⚠️ | 新 JDK 版本要显式开启 ✓ |
| 6 | Windows 上折腾火焰图 ✗ | 换 Linux,或改用 JFR ✓ |
DiagnoseTarget,用 jps 找到它 ✓✓;thread -n 3、thread -b、trace -n 3、watch -n 3、sc -d、jad ✓✓✓;jcmd Thread.print 交叉验证死锁 ✓✓;DiagnoseTarget 的慢方法改成「先注释掉 sleep」再 trace:验证耗时占比随之变化 ✓✓;stop 卸载 Arthas,再用 jps 确认 ✓✓✓。验收方式:给一个「CPU 高 + 接口慢」的现场,能说出先跑哪三条命令、每条命令看什么、结论怎么下。
扩展任务:给你自己的项目 A 写一份《线上诊断手册》:三条线各一个命令清单 + 授权流程 + 取证留档要求 ✓✓
sc -d 输出里的 classLoaderHash 有什么用?-n?thread -n 3 / trace 类 方法 / thread -b ✓✓★《字节码与 Java Agent》 字节码与 Java Agent —— 亲手实现「Arthas 背后的那套机制」。 Linux 服务端的同类排查思路见 《进程与端口排查》(进程与端口排查)。
暂无