简介:线上 Java 应用出现 CPU 飙升、接口响应变慢或疑似死锁时,能否快速定位根因,直接决定故障恢复时长。本文以 JVM 自带的堆栈分析工具为核心,系统讲解其常用选项与采集技巧,深入解读线程六大状态的含义与易踩的认知陷阱,梳理堆栈文件中的关键信号,并结合 CPU 占用过高、死锁、锁竞争、外部依赖拖慢四个典型场景,给出一套可复用的排查方法论,帮助你高效完成线上故障定位与恢复。
线上 Java 应用的故障虽然表象各异,但归纳起来基本落在四个资源维度:CPU、内存、线程与外部 IO。动手之前先花一分钟对症状归类,能避免盲目敲命令浪费时间:
JDK 自带的命令行诊断工具基本能覆盖上述场景:用 jps 列出目标 Java 进程,用 jstat 观察 GC 行为,用 jmap 导出内存快照,用 jstack 导出线程堆栈。其中 jstack 是线程类问题的核心武器,也是本文的重点。
jstack 用于生成指定 Java 进程在某一时刻的线程堆栈快照(thread dump),输出内容包括:每个线程当前执行的方法调用链、线程持有的锁、正在等待的锁以及线程当前状态。它相当于给进程内所有线程拍了一张“X 光片”,是定位死锁、锁竞争、线程阻塞与 CPU 热点的第一手数据。
最常用的命令形式如下:
# 打印对应 Java 进程的线程堆栈jstack [option] <pid>
常用的三个选项:
需要特别强调:堆栈是某一瞬间的切面,单次快照很容易误判。规范做法是间隔 3~5 秒连续采集 3~5 次,横向对比,只关注多次快照中反复出现的共同特征,才能区分偶发现象与持续性问题。
Java 线程定义了六种状态:
状态流转可以概括为:New 经 start() 进入 RUNNABLE,RUNNABLE 与 Blocked、Waiting、Timed Waiting 之间因锁竞争或主动等待而切换,最终随 run() 结束进入 Terminated。
第一,RUNNABLE 不等于“正在消耗 CPU”。阻塞在 socket 读写上的线程,在 JVM 视角下同样显示为 RUNNABLE,因为从 JVM 看来它“随时可被调度”。判断线程是否真在干活,必须结合堆栈内容而非只看状态标签。
第二,Waiting 不一定是问题。线程池中的空闲工作线程通常以 parked/waiting 形式挂起,属于健康状态。判断异常的关键不是单个线程的状态,而是“大量线程处于同一状态且堆栈指向同一处代码”。
拿到 dump 文件后,应优先全文检索以下关键字:
# 1. 定位占用 CPU 最高的 Java 进程top# 2. 查看进程内各线程的 CPU 占用情况top -Hp <pid># 3. 将目标线程号转为十六进制(堆栈中的 nid 为十六进制)printf '%x\n' <tid># 4. 从堆栈中提取对应线程,查看其正在执行的方法jstack <pid> | grep '0x<十六进制tid>' -A 30
思路说明:线程号必须转成十六进制,因为堆栈中用 nid 标识线程。-A 30 会输出匹配行之后的 30 行调用链,正在执行的方法就在调用栈顶部。多数情况下可定位到死循环、频繁正则匹配、大集合遍历或序列化热点代码。若对比多次快照发现最耗 CPU 的其实是 GC 线程,则问题方向应转向内存:用 jstat 观察 GC 频率,用 jmap 导出堆快照分析对象构成。
特征是应用部分功能整体无响应且永不自愈。使用 jstack -l pid 采集堆栈分析:
处置上,死锁无法自愈,线上只能通过回滚或重启恢复;根治必须回到代码层:统一加锁顺序、缩小锁的持有范围、使用带超时的锁获取方式,避免在持锁状态下发起外部调用。
当堆栈中出现成片的 blocked / waiting for monitor entry,且等待的是同一个 locked 对象时,说明锁竞争已成为吞吐瓶颈。常见根因包括:粗粒度的 synchronized 方法(如全局缓存刷新、全局配置更新)、临界区内包含数据库访问或文件 IO 等慢操作。优化方向包括:缩小临界区范围、改用读写锁或分段锁、用原子类与 CAS 消除锁、将临界区内的慢操作异步化外移。
特征为大量线程 waiting on condition,堆栈指向获取数据库连接、等待下游 HTTP 响应等位置。此时重点核对三件事:连接池大小与超时配置是否合理、下游服务是否出现性能劣化、是否存在重试风暴放大调用压力。代码层面必须为所有外部调用设置超时与失败兜底策略,避免一个依赖拖垮整个进程。
JVM 故障排查的本质是一条“证据链”推理过程:先按症状把问题归入 CPU、内存、线程、IO 中的某一域,再用对应工具采集证据。线程域的证据就是线程堆栈——先读状态(理解六大状态的含义与陷阱),再扫关键字(deadlock、blocked、waiting on condition 等),最后结合调用栈与业务代码收敛到具体方法。jstack 这类 JDK 自带工具轻量、零依赖、随取随用,是每一位 Java 开发者都应熟练掌握的基本功。掌握本文的思路与套路后,绝大多数线程类线上故障都能在几分钟内定位到代码级根因。