背景
jstack 用来查看某个 Java 进程内的线程堆栈信息,语法:
jstack [option] PID
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip
常用参数:
| 参数 | 说明 |
|---|---|
-l |
long listing,额外打印锁信息;发生死锁时可用 jstack -l pid 观察锁持有情况 |
-m |
mixed mode,同时输出 Java 堆栈和 C/C++(Native 方法)堆栈 |
-F |
当 jstack [-l] pid 无响应时强制 dump |
现代 JDK(含 JDK 26)里还有替代吗
有,而且是官方推荐的替代:jcmd <pid> Thread.print。
jstack、jmap、jinfo 这几个独立小工具早已被 Oracle 标记为 experimental / unsupported,官方故障排查文档现在统一推荐通过 jcmd 这个诊断入口来做线程栈、堆 dump、JFR 等操作:
jcmd <pid> Thread.print # equivalent to: jstack <pid>
jcmd <pid> Thread.print -l # equivalent to: jstack -l <pid>, with lock info
jstack 目前仍随 JDK 26 一起提供,还能正常使用,只是不建议在新脚本 / 新流程里继续依赖它。参数与用法详见 jcmd;更完整的生产诊断工具对比(Arthas、async-profiler、JFR 等)见 生产环境诊断工具选型。
实战:定位 CPU 占用最高的线程
用到的命令:ps、top、printf、jstack、grep。
第一步,找到 Java 进程 PID(示例应用名为 mrf-center):
ps -ef | grep mrf-center | grep -v grep
# root 21711 1 1 14:47 pts/3 00:02:10 java -jar mrf-center.jar
得到进程 PID 为 21711。第二步,找出该进程内最耗费 CPU 的线程,可以用 ps -Lfp pid、ps -mp pid -o THREAD,tid,time 或 top -Hp pid,这里用第三种:
top -Hp 21711
TIME 列就是各个 Java 线程耗费的 CPU 时间。假设 CPU 时间最长的线程 ID 是 21742,把它转成十六进制(jstack 输出里线程 ID 是十六进制):
printf "%x\n" 21742
# 54ee
第三步,用 jstack 输出进程 21711 的堆栈信息,再按线程 ID 的十六进制值过滤:
jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]
可以看到 CPU 消耗在 PollIntervalRetrySchedulerThread 这个类的 Object.wait() 上,对应到业务代码里是一段轮询任务的空闲等待逻辑:
// Idle wait
getLog().info("Thread [" + getName() + "] is idle waiting...");
schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting;
long now = System.currentTimeMillis();
long waitTime = now + getIdleWaitTime();
long timeUntilContinue = waitTime - now;
synchronized (sigLock) {
try {
if (!halted.get()) {
sigLock.wait(timeUntilContinue);
}
} catch (InterruptedException ignore) {
}
}
sigLock.wait(timeUntilContinue) 就对应了前面 dump 里的 Object.wait()。
注意事项
- 不同 JVM 实现、不同版本的线程 dump 格式都不一样;下文示例以早期 HotSpot(5.0/6)为例,字段含义在现代 HotSpot 上基本沿用。
- 一次 dump 往往不足以确认问题,建议连续产生三次 dump,如果每次都指向同一个问题,才能确认问题的典型性。
JVM 内置线程
线程 dump 里除了业务线程,还有一些 JVM 内部的后台线程,比如负责垃圾回收、低内存检测等任务,这些线程在 JVM 初始化时就已存在:
"Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000]
"CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798]
"Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000]
"Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8]
at java.lang.Object.wait(Native Method)
- waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
- locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)
"Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38]
at java.lang.Object.wait(Native Method)
- waiting on <0xef600758> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:474)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
- locked <0xef600758> (a java.lang.ref.Reference$Lock)
"VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable
"VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition
这几个内置线程的作用见 DestroyJavaVM 与 JVM 内置线程。
更多时候需要观察的是用户级别的线程:
"Thread-1" prio=10 tid=0x08223860 nid=0xa waiting on condition [0xef47a000..0xef47ac38]
at java.lang.Thread.sleep(Native Method)
at testthread.MySleepingThread.method2(MySleepingThread.java:53)
- locked <0xef63d600> (a testthread.MySleepingThread)
at testthread.MySleepingThread.run(MySleepingThread.java:35)
at java.lang.Thread.run(Thread.java:595)
从这段 dump 能看到三类信息:线程状态(waiting on condition)、线程的调用栈、线程当前锁住的资源(<0xef63d600>)。
线程状态解读
线程状态是 dump 里的一个重要指标,显示在每个线程 stacktrace 首行结尾处。
Runnable
表示线程具备运行条件,正在运行队列中等待操作系统调度,或者正在运行。
Wait on condition
表示线程在等待某个条件发生,具体原因要结合 stacktrace 分析。最常见的情况是等待网络读写:数据没准备好时线程阻塞,数据就绪后重新被激活。在 Java 引入 NIO 之前,每个网络连接都对应一个线程处理读写,即使没有数据可读写线程也会阻塞,容易造成资源浪费和调度压力;NIO 引入之后这个问题得到缓解。
如果发现大量线程都处于 Wait on condition 且从 stack 看是在等待网络读写,可能是网络瓶颈的征兆,需要结合 netstat、CPU 利用率(尤其是系统态占比)等工具综合判断。另一种常见情况是线程在 sleep,等到睡眠时间到了才会被唤醒。
Waiting for monitor entry 与 in Object.wait()
Java 多线程之间的同步依赖 Monitor:每个对象都有且仅有一个 monitor,某一时刻只能被一个线程持有。持有 monitor 的线程是 “Active Thread”,其它线程分别在 “Entry Set” 和 “Wait Set” 两个队列里等待:
- 在 “Entry Set” 中等待的线程,状态显示为
waiting for monitor entry - 在 “Wait Set” 中等待的线程,状态显示为
in Object.wait()
线程申请进入被 synchronized 保护的临界区时,如果该 monitor 未被占用、Entry Set 也没有其他等待线程,本线程直接成为 owner 并执行临界区代码,状态为 Runnable;如果 monitor 已被占用,本线程进入 Entry Set 排队,状态为 waiting for monitor entry:
"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8]
at testthread.WaitThread.run(WaitThread.java:39)
- waiting to lock <0xef63bf08> (a java.lang.Object)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
临界区的作用是保证内部代码执行的原子性和完整性,但同一时间只允许一个线程串行通过。如果 synchronized 使用不当或过度使用,会导致大量线程堆积在临界区入口,系统性能大幅下降;线程 dump 中若发现这种情况,应该审查源码。
线程获得 monitor、进入临界区后,如果发现继续运行的条件不满足,会调用该对象的 wait() 方法主动放弃 monitor,进入 Wait Set 排队,直到有其他线程调用 notify() / notifyAll() 才有机会重新竞争 monitor。这种线程在 dump 中表现为 in Object.wait():
"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38]
at java.lang.Object.wait(Native Method)
- waiting on <0xef63beb8> (a java.util.ArrayList)
at java.lang.Object.wait(Object.java:474)
at testthread.MyWaitThread.run(MyWaitThread.java:40)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
注意这段 dump 里先 locked 又 waiting on 同一个对象,对应的代码是:
synchronized (obj) {
// ...
obj.wait();
// ...
}
线程先用 synchronized 获得对象的 monitor(对应 locked),执行到 obj.wait() 时放弃 monitor 所有权,进入 Wait Set(对应 waiting on)。多个结构相似的线程同时出现类似 dump 也是正常现象,比如多个消费者线程从同一个队列读数据:队列为空时它们都在这个队列对象上等待,队列来了数据后被 notify,但只有一个线程能抢到 lock 继续执行,其余线程继续等待。
JDK 5.0 的 Lock
synchronized / monitor 机制使用不当容易造成性能问题。JDK 5.0 引入了 java.util.concurrent.locks.Lock,让开发者能更灵活地开发高性能并发程序,可以替代 synchronized / monitor。但 Lock 只是一个普通类,JVM 无法感知 Lock 对象的占用情况,因此线程 dump 中不会包含 Lock 相关信息——排查死锁等问题时不如 synchronized 方式直观。
案例分析
死锁
多线程程序里同步机制使用不当,可能造成死锁,表现为程序停顿或不再响应请求:
"Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000..0x02d3fd68]
at deadlockthreads.TestThread.run(TestThread.java:31)
- waiting to lock <0x22c19f18> (a java.lang.Object)
- locked <0x22c19f20> (a java.lang.Object)
"Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000..0x02cff9e8]
at deadlockthreads.TestThread.run(TestThread.java:31)
- waiting to lock <0x22c19f20> (a java.lang.Object)
- locked <0x22c19f18> (a java.lang.Object)
JDK 5 加强了死锁检测,线程 dump 会直接报告 Java 级别的死锁:
Found one Java-level deadlock:
"Thread-1":
waiting to lock monitor 0x0003f334 (object 0x22c19f18, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0003f314 (object 0x22c19f20, a java.lang.Object),
which is held by "Thread-1"
热锁
热锁也是导致系统性能瓶颈的常见原因,表现为多个线程对临界区或锁激烈竞争:
- 频繁的线程上下文切换:线程因等待资源阻塞时被操作系统切出,获得资源后再切入
- 大量系统调用:上下文切换和热锁竞争都会带来额外的系统调用
- 大部分 CPU 时间花在系统态:应用虽然很忙,但用户态 CPU 占比很低,业务代码得不到充分的 CPU 资源
- CPU 数目越多,性能反而可能越差:并发线程越多,上下文切换和系统态开销越大
排查思路是结合操作系统资源监控工具与线程 dump,找出线程都阻塞在哪些方法上。例如曾经遇到大多数线程都处于 waiting for monitor entry 或 in Object.wait(),且都阻塞在压缩 / 解压缩方法上;换用第三方压缩库替代 JDK 自带实现后,系统性能提升了几倍。
参考
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-07-16 | categories 由 inbox 改为 Java;重新分节整理正文,修复代码块缺少语言标识、乱码断行等格式问题;新增「现代 JDK(含 JDK 26)里还有替代吗」一节,说明官方推荐用 jcmd Thread.print 替代;补充与 jcmd.md、destroy-java-vm.md、java-production-diagnostics-tooling.md 的站内互链;标签由 reprint 改为 remix、AI-assisted |
内容与站内其他 Java 诊断工具文章一致按规范整理,并回答作者关于现代 JDK 是否有 jstack 替代的提问 |