这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 线程 dump 是 JVM 中所有 Java 线程在某时刻的栈快照,通常称为 thread dump 或 javacore。它是分析 CPU 高、接口阻塞、死锁、线程池耗尽和外部依赖故障的基础证据。
单次线程 dump 只是一个瞬间,生产问题通常需要连续采集多次。
25.1 获取线程 dump
使用 jstack:
jstack <pid> > thread-1.txt
强制输出:
jstack -F <pid> > thread-forcely.txt
使用 jcmd:
jcmd <pid> Thread.print > thread-1.txt
Linux 可通过信号:
kill -3 <pid>
输出通常会写到 JVM 标准输出日志,而不是命令行终端。
25.2 采集策略
建议连续采集:
for i in 1 2 3 4 5; do
jcmd <pid> Thread.print > "thread-$i.txt"
sleep 2
done
同时记录:
- 时间戳;
- CPU 使用率;
- GC 日志;
- 业务日志;
- 请求 QPS 和错误率;
- 容器 CPU limit;
- 数据库和下游状态。
连续 dump 的价值在于比较:一个线程短暂 WAITING 正常,长期卡在同一个调用则异常。
25.3 线程状态
常见状态:
| 状态 | 含义 | 常见原因 |
|---|---|---|
| RUNNABLE | 可运行,可能正在执行或等待 CPU | 计算密集、CPU 不足 |
| BLOCKED | 等待进入 monitor | synchronized 竞争 |
| WAITING | 等待被显式唤醒 | join、wait、LockSupport.park |
| TIMED_WAITING | 限时等待 | sleep、带超时等待 |
| NEW | 尚未启动 | 刚创建 |
| TERMINATED | 已结束 | 正常结束 |
注意:Java 线程显示 RUNNABLE 时,不一定正在消耗 CPU。例如 socket read 可能映射为 RUNNABLE,但实际在等待内核网络事件。
25.4 线程栈结构
简化示例:
"http-nio-8080-exec-3" #42 daemon prio=5 os_prio=0 tid=... nid=... waiting on condition
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:...)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(...)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:...)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:...)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:...)
at java.lang.Thread.run(Thread.java:...)
阅读顺序:
- 线程名,判断角色;
- 状态;
- 栈顶,判断阻塞点;
- 业务包名,定位代码;
- 锁信息,判断持有者和等待者。
25.5 死锁分析
jvm 自动检测到的死锁会在 dump 末尾输出:
Found one Java-level deadlock:
=============================
"thread-A":
waiting to lock monitor ... object ...,
which is held by "thread-B"
"thread-B":
waiting to lock monitor ... object ...,
which is held by "thread-A"
手动分析步骤:
1. 找 waiting to lock 的线程
2. 找锁被哪个线程持有
3. 查看持有者在做什么
4. 画出锁依赖环
5. 检查加锁顺序
修复方向:
- 统一加锁顺序;
- 使用 tryLock 加超时;
- 缩小锁范围;
- 拆分锁粒度;
- 避免在锁内调用外部 IO;
- 使用无锁结构或队列化写。
25.6 线程池耗尽
典型特征:
- 业务线程数量等于 maximumPoolSize;
- 大量线程阻塞在同一个外部调用;
- 队列持续增长;
- 出现 RejectedExecutionException;
- CPU 反而很低。
示例:
"order-worker-1" RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at ...HttpClient.execute(...)
at com.demo.rpc.PayClient.call(...)
at com.demo.task.OrderWorker.run(...)
如果 20 个 worker 全部卡在 PayClient.call,问题多半是支付服务慢或超时配置缺失,而不是线程池太小。
处理顺序:
1. 给外部调用设置超时
2. 隔离不同下游的线程池
3. 熔断和降级
4. 排查下游故障
5. 再评估线程数
25.7 CPU 高分析
线程 dump 与操作系统线程号关联:
nid=0x1a2b
Linux 排查:
top -H -p <pid>
printf "%x\n" <tid>
然后在 dump 中搜索对应 nid=0x...。
Windows 可用 Process Explorer 或 PowerShell 查看线程 CPU,再结合 dump 分析。
高 CPU 常见栈:
| 栈顶方向 | 可能原因 |
|---|---|
| 正则解析 | 回溯灾难 |
| JSON/XML 解析 | 大报文、复杂结构 |
| 序列化 | 循环序列化大对象 |
| 集合操作 | 大 List 排序、过滤 |
| GC 线程 | GC 压力 |
| JIT 编译线程 | 启动期或热点重编译 |
25.8 锁竞争分析
特征:
- 多个线程 BLOCKED;
- waiting to lock 同一对象;
- 持有者长期不释放;
- 响应时间抖动。
分析模板:
等待锁:0x000000076ae12345
等待线程:order-1, order-2, order-3
持有线程:report-worker-1
持有线程栈:
com.demo.ReportService.buildLargeReport(...)
com.demo.ReportController.download(...)
结论:下载大报表持有全局锁,订单请求排队。修复可以是将报表生成移到异步任务、去掉全局锁或按租户分锁。
25.9 常见误判
| 误判 | 实际情况 |
|---|---|
| WAITING 就是故障 | 线程池空闲时正常等待任务 |
| RUNNABLE 就是耗 CPU | 可能阻塞在 Native IO |
| 单次 dump 就下结论 | 偶发栈无法代表持续状态 |
| 线程多就是泄漏 | 池大小配置合理时线程数稳定 |
| CPU 高一定是业务死循环 | 可能是 GC、JIT、压缩、序列化 |
线程 dump 只描述栈和状态,最终要结合操作系统线程 CPU、监控和业务日志。
本章小结
线程 dump 是分析阻塞、死锁、线程池耗尽和 CPU 异常的基础证据。生产采集应连续多次,并同时保存 CPU、GC 和业务指标。分析时先看线程名和状态,再看栈顶业务代码和锁关系,最后结合多快照比较确认线程是否持续停留在同一位置。
思考题
- 为什么单次线程 dump 不足以定位偶发慢请求?
- RUNNABLE 线程一定在消耗 CPU 吗?
- 如何从线程 dump 找到死锁依赖环?
- 线程池满且 CPU 低,优先排查什么?
- 如何把 Linux 线程 ID 和 Java 线程栈对应起来?