JVM 统一诊断工具 jstack 使用

jstack 深度实战指南

jstack (Stack Trace for Java) 用于生成虚拟机当前时刻的线程快照(Thread Dump)。它是定位线程长时间停顿、CPU 占用过高、死锁、无法响应请求等问题的“听诊器”。


1. 命令参数详解

基本语法:jstack [options] <PID>

参数 示例 作用
无参数 jstack 1234 最常用。输出标准线程堆栈。
-l jstack -l 1234 Long listing。除堆栈外,显示关于锁的附加信息(如 Ownable Synchronizers,即 ReentrantLock 等 JUC 锁)。建议默认加上。
-F jstack -F 1234 Force。当进程无响应(hung 住)时强制打印堆栈。注意:此操作可能会让进程暂停更久,谨慎使用。
重定向 jstack 1234 > dump.log 将输出保存到文件,便于离线分析或发送给同事。

2. 读懂线程堆栈日志(解剖图)

一行标准的线程日志包含丰富的信息,我们需要学会“拆解”它。

日志示例:

Plaintext

"http-nio-8080-exec-1" #12 daemon prio=5 os_prio=0 tid=0x00007f... nid=0x1a2b waiting on condition [0x00007f...]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        ...

字段逐一解析:

  1. Thread Name ("http-nio-8080-exec-1"): 线程名称。最佳实践: 在代码中给线程池自定义名称(如 order-process-thread),排查问题时能一眼看出业务归属。
  2. daemon: 标记是否为守护线程。
  3. prio=5: Java 代码层面的优先级。
  4. tid: Java 内存中的线程 ID。
  5. nid (0x1a2b): 关键字段! Native Thread ID,即操作系统层面的线程 ID(16进制)。
    • 核心用途:用于将 Linux 的 top -Hp 显示的 PID(10进制)与这里的堆栈对应起来。
  6. Status: 线程的大致状态。
  7. java.lang.Thread.State: 具体的 Java 线程状态(详见下文)。

3. 核心线程状态 (Thread State) 详解

这是分析问题的核心,必须理解这几种状态的区别:

状态 含义 风险等级 排查思路
RUNNABLE 正在运行,或者在等待 CPU 时间片,或者在进行系统调用(如网络读取)。 ⭐️ / ⭐️⭐️⭐️ 如果大量线程是 RUNNABLE 且代码位置相同,通常是性能瓶颈或死循环。
BLOCKED 被阻塞。正在等待获取 synchronized 锁。 ⭐️⭐️⭐️⭐️⭐️ 高危。说明并发竞争非常激烈。如果长时间不释放,会导致雪崩。
WAITING 无限期等待。调用了 wait()join()LockSupport.park() ⭐️⭐️ 需要其他线程来唤醒(notify/unpark)。如果是线程池空闲线程,则正常。
TIMED_WAITING 限时等待。调用了 sleep(t)wait(t) ⭐️ 通常正常。但如果大量线程都在 sleep,响应也会变慢。

4. 经典场景实战分析

场景一:CPU 飙升 100% (定位高耗时代码)

这是 jstack 最典型的用法,结合 Linux 命令使用。

  1. 定位进程: top (假设 PID 为 10997)。

  2. 定位线程: top -Hp 10997。观察哪个线程 %CPU 最高(假设是 11005)。

  3. 换算进制: 将 11005 转为 16 进制。

    Bash

    printf "%x\n" 11005
    # 输出 2aff
    
  4. 精准查找:

    Bash

    jstack 10997 | grep -A 20 "2aff"
    

    结果解读:

    你看到的堆栈顶层代码,就是正在疯狂消耗 CPU 的逻辑。常见的有:复杂计算、死循环、大量字符串拼接、频繁 GC(堆栈会显示 GC 线程)等。

场景二:死锁 (Deadlock)

当两个线程互相持有对方需要的锁时,就会死锁。jstack 会自动检测并打印在日志末尾。

日志特征:

搜索关键字 Found one Java-level deadlock。

Plaintext

Found one Java-level deadlock:
=============================
"Thread-A":
  waiting to lock monitor 0x000000005... (object 0x000000076...)
  locked 0x000000075...
"Thread-B":
  waiting to lock monitor 0x000000075... (object 0x000000075...)
  locked 0x000000076...

解决:根据堆栈显示的类名和行号,优化加锁顺序或使用 tryLock

场景三:应用无响应,但 CPU 很低 (外部资源卡顿)

应用突然不处理请求了,但是 CPU 使用率几乎为 0。这通常是线程都在等待外部资源。

分析步骤

  1. 导出堆栈:jstack <PID> > dump.log

  2. 统计线程状态:

    Bash

    grep "java.lang.Thread.State" dump.log | sort | uniq -c
    
  3. 如果发现大量 RUNNABLE 状态,但 CPU 很低,检查堆栈内容,是否卡在 Socket 读取上:

    Plaintext

    at java.net.SocketInputStream.socketRead0(Native Method)
    

    结论:这说明数据库查询极慢、Redis 卡顿或调用的第三方 HTTP 接口没有响应,导致线程池被耗尽。

场景四:大量 BLOCKED (锁竞争)

如果发现大量线程状态为 BLOCKED (on object monitor),且都在等待同一个对象地址。

Plaintext

"Thread-5" prio=5 tid=0x01... nid=0x2... blocked
  - waiting to lock <0x0000000780a000b0> (a java.lang.Object)
  - waiting for monitor entry [0x00000000...]

分析:找到持有锁 <0x0000000780a000b0> 的线程(搜索 locked <0x0000000780a000b0>),查看它为什么执行这么慢(可能在做 IO 或复杂计算)。


5. 高级技巧与注意事项

  1. 多次采样 (重要)

    一次 dump 只是瞬间快照。如果怀疑某个线程卡死,建议间隔 5 秒执行 3 次 jstack。

    • 如果 3 次结果中,该线程一直停在同一行代码,说明真的卡死了。
    • 如果一直在变动,说明只是执行慢,但还在跑。
  2. 现代替代品: jcmd

    在 JDK 8 以后,官方推荐使用 jcmd,功能更全。

    jcmd <PID> Thread.print
    
  3. 在线分析工具

    在线可视化工具:

    • FastThread.io (业界最强,推荐)
    • JProfiler / VisualVM (本地 GUI 工具)
posted @ 2025-12-04 17:15  Sappy  阅读(1)  评论(0)    收藏  举报