JVMNotes

第 25 章:线程 dump 分析

zjc 于 2026-01-25 发布

这是《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

同时记录:

  1. 时间戳;
  2. CPU 使用率;
  3. GC 日志;
  4. 业务日志;
  5. 请求 QPS 和错误率;
  6. 容器 CPU limit;
  7. 数据库和下游状态。

连续 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:...)

阅读顺序:

  1. 线程名,判断角色;
  2. 状态;
  3. 栈顶,判断阻塞点;
  4. 业务包名,定位代码;
  5. 锁信息,判断持有者和等待者。

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. 检查加锁顺序

修复方向:

  1. 统一加锁顺序;
  2. 使用 tryLock 加超时;
  3. 缩小锁范围;
  4. 拆分锁粒度;
  5. 避免在锁内调用外部 IO;
  6. 使用无锁结构或队列化写。

25.6 线程池耗尽

典型特征:

  1. 业务线程数量等于 maximumPoolSize;
  2. 大量线程阻塞在同一个外部调用;
  3. 队列持续增长;
  4. 出现 RejectedExecutionException;
  5. 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 锁竞争分析

特征:

  1. 多个线程 BLOCKED;
  2. waiting to lock 同一对象;
  3. 持有者长期不释放;
  4. 响应时间抖动。

分析模板:

等待锁: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 和业务指标。分析时先看线程名和状态,再看栈顶业务代码和锁关系,最后结合多快照比较确认线程是否持续停留在同一位置。

思考题

  1. 为什么单次线程 dump 不足以定位偶发慢请求?
  2. RUNNABLE 线程一定在消耗 CPU 吗?
  3. 如何从线程 dump 找到死锁依赖环?
  4. 线程池满且 CPU 低,优先排查什么?
  5. 如何把 Linux 线程 ID 和 Java 线程栈对应起来?