凌晨一点四十七分,监控群里被一张 top 截图刷屏:支付网关服务器的 CPU 已经顶满了。我登录上去,熟练地敲下两行命令,拿到一份线程转储。文件打开一看,我反而更紧张了——整个 JVM 里只有 36 个线程,干干净净,没有几百个卡在锁上的线程池,也没有明显挂着 Waiting 的灵异线程。按以往的经验,我本来应该从一堆可疑线索里捞凶手,但这次摆在眼前的是一份看起来毫无破绽的现场。
我盯着这份 dump 看了十分钟,心里冒出一种不太好的预感:JVM 线程诊断最难的场景,不是几百个线程里捞一个异常线程,而是当线程数少、状态正常、调用栈干净,你反而要花更多力气去证明哪个“完美嫌疑人”才是真凶。 这篇文章就借这次排查过程,聊聊我从 36 个线程里排除一个又一个疑点、最后锁定真凶的完整链路,以及我在实操中踩过的误判坑。
1. 第一时间拿到可信的快照:线程转储的采集不只是“敲一条命令”
线程转储是 JVM 诊断的基础证据,很多人上来就敲 jstack -l <pid>,把文件丢到一边开始猜。这一步看起来简单,实际上不同场景下采集方式会直接影响判断结果。
1.1 四种常见采集方式的差异
我平时常用的线程转储方式有四种,适用场景完全不同:
| 采集方式 | 命令 | 特点 | 适合场景 |
|---|---|---|---|
| jstack | jstack -l <pid> |
最通用,带锁信息 | 常规排查、现场快照 |
| jcmd | jcmd <pid> Thread.print -l |
JDK 自带,不依赖额外工具 | 容器内、受限环境推荐 |
| kill -3 | kill -3 <pid> |
输出到 JVM 标准输出 | 进程卡死、jstack 无法 attach 时 |
| JMX 工具 | jconsole / VisualVM | 可视化,可远程 | 本地开发、测试环境 |
这次线上环境是容器部署,JVM 的 PID 通常是 1,直接 kill -3 1 不一定能把输出导到你手上,因为标准输出会被容器日志层接管。我习惯先用 jcmd <pid> Thread.print -l > /tmp/dump1.txt,如果 jcmd 也拿不到线程,才考虑 kill -3 配合查看容器日志。
1.2 单次转储和多次采样的决策
很多人拿一份 dump 就开始分析,我建议至少间隔 10 到 15 秒连续抓三次。原因很直白:线程状态是瞬时快照,不是持续录像。 一个正在执行正则匹配的线程,抓它的时候恰好落入一个快速分支,栈可能只有三层;但你隔 15 秒再抓,它可能又回到同一个方法。如果三次都停在同一个栈顶,这个线索的可信度会高很多。
这次排查我抓了三次,每次间隔 12 秒。第一次看到 pay-callback-pool-1-thread-7 处于 RUNNABLE,第二次、第三次它依然在同一个方法附近。这时候我才决定深入追这条线程,而不是直接去看其他几个看起来更“异常”的 Waiting 线程。
提示:线程转储建议在 CPU 持续飙高时采集,如果 CPU 已经恢复正常,你抓到的 dump 很可能没有参考价值。先确认故障还在
