什么是 Java 线程堆栈?🔖
简单说,线程堆栈(Thread Stack) 就是 Java 程序中每个线程 “正在做什么” 的 “实时快照”。它记录了线程当前的状态(如运行中、阻塞、等待等)、调用的方法链条(从当前执行的方法一直到最开始的入口方法),以及每个方法在代码中的位置(行号)。
打个比方:就像你看电影时按下暂停键,屏幕上会显示每个角色正在做的动作和所处的场景 —— 线程堆栈就是 Java 程序的 “暂停快照”,能帮你分析线程卡住、死锁、性能问题等。
我们用一个生活场景类比,再结合实际代码和线程堆栈输出,来详细理解线程堆栈。
为什么需要线程堆栈?🔖
当程序出现问题(比如卡着不动、响应慢)时,光看代码很难定位原因。线程堆栈能告诉你:
- 哪个线程在 “干活”,哪个线程在 “偷懒”(阻塞 / 等待);
- 线程卡住时正在调用哪个方法,是不是卡在了某个锁上;
- 有没有死锁(两个线程互相等对方的锁)。
- 系统无缘无故CPU过高
- 系统挂起,无响应
- 系统运行越来越慢
- 性能瓶颈,如无法充分利用CPU等
- 线程死锁、死循环,饿死等
- 线程数量太多导致系统失败,如无法创建线程等
Java dump 线程堆栈信息的方法?🔖
在 Java 中,获取线程堆栈堆栈信息(Thread Dump)是排查线程死锁、阻塞、CPU 过高、响应缓慢等问题的重要手段。以下是几种常用的线程堆栈信息导出方法,适用于不同场景:
使用 JDK 自带命令(最常用):JDK 提供了多个命令行工具,可直接导出线程堆栈,无需额外依赖。
🔹jstack 命令(推荐)
jstack 是专门用于打印 Java 线程堆栈的工具,支持 Windows、Linux、macOS 等系统,用法简单直接。
- 获取 Java 进程 PID
先通过 jps 或 ps 命令找到目标 Java 进程的 PID:
# 方法1:JDK自带jps(直接显示Java进程PID和主类名) jps # 输出示例:12345 MyApplication (12345即为PID) # 方法2:通用ps命令(适用于所有系统) ps -ef | grep java # Linux/macOS tasklist | findstr java # Windows
- 导出线程堆栈
使用 jstack -l <PID> 导出堆栈(-l 表示显示锁信息,有助于排查死锁):
# 直接输出到控制台 jstack -l 12345
🔹jcmd 命令(JDK 7+)
jcmd 是 JDK 7 新增的多功能命令工具,可替代 jstack、jmap 等多个命令,用法更统一。
- 同样先通过 jps 获取 PID。
- 执行以下命令导出线程堆栈:
# 输出到控制台 jcmd 12345 Thread.print # 输出到文件 jcmd 12345 Thread.print > thread_dump.txt
功能更全面,除了线程堆栈,还能执行其他 JVM 诊断命令(如 GC、类信息等)。
🔹JConsole(JDK 自带)
对于不熟悉命令行的开发者,可使用图形化工具直观获取线程堆栈。
JConsole 是 JDK 内置的监控工具,支持线程堆栈导出:
- 启动 JConsole:命令行输入 jconsole,选择目标 Java 进程连接。
🔹VisualVM(JDK 6-8 自带,高版本需单独下载)
VisualVM 是更强大的可视化工具,支持线程堆栈、内存分析等:
- 启动 VisualVM:命令行输入 jvisualvm(JDK 9 + 需从官网下载)。
- 选择目标进程,切换到「线程」标签,点击「线程 dump」按钮导出堆栈。
dump 本地线程堆栈信息的方法?🔖
在 Java 中,“本地线程” 通常指执行本地方法(Native Method)的线程,其堆栈信息包含 Java 层调用和本地代码(如 C/C++)的执行轨迹。导出本地线程堆栈的方法与普通 Java 线程堆栈类似,但需要特别关注本地方法的调用链。以下是具体方法:
Java 线程堆栈默认会包含本地方法的调用信息(如native方法的入口),使用jstack或jcmd即可导出,无需额外配置。
🔹使用jstack导出(推荐)
# 1. 获取Java进程PID(同普通线程) jps # 或 ps -ef | grep java # 2. 导出包含本地方法信息的堆栈(-l参数会显示更多锁和本地方法细节) jstack -l <PID> > native_thread_dump.txt
堆栈中本地线程的特征:
在导出的堆栈中,执行本地方法的线程会显示native method标记,例如:
"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fd2a30c000 nid=0x5703 runnable [0x000070000f9f3000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) # 本地方法 at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) ...
java.net.SocketInputStream.socketRead0(Native Method)
表示该线程正在执行本地方法socketRead0
。
🔹使用jcmd导出
jcmd <PID> Thread.print > native_thread_dump.txt
效果与jstack一致,会包含本地方法的调用信息。
进阶:获取本地代码(C/C++)的详细堆栈
如果需要分析本地方法内部的 C/C++ 代码调用链(如 JDK 源码中的本地实现或自定义 JNI 代码),需要额外工具配合,因为 JDK 自带工具只能显示 Java 层到本地方法的入口,无法深入本地代码内部。
使用操作系统级工具(针对本地代码):不同操作系统有内置工具可捕获进程的所有线程(包括 Java 线程和本地线程)的详细堆栈
🔹Linux/macOS:pstack 或 gdb
- pstack(简单直接):
# 安装pstack(Linux可能需要yum install pstack) pstack <PID> > native_code_dump.txt
输出会包含本地线程的 C 函数调用链,例如 JVM 内部的本地方法实现:
#0 0x00007f8a8a5f4100 in read () from /lib64/libc.so.6 #1 0x00007f8a8b0e6a3d in Java_java_net_SocketInputStream_socketRead0 () from /usr/lib/jvm/jdk1.8.0_361/jre/lib/amd64/libnet.so #2 0x00007f8a85f1a9c8 in ?? () ...
- gdb(更强大,适合调试):
# 启动gdb附加到进程 gdb -p <PID> # 在gdb中获取所有线程堆栈 (gdb) thread apply all bt # 保存到文件 (gdb) set logging file native_gdb_dump.txt (gdb) set logging on (gdb) thread apply all bt (gdb) set logging off
🔹Windows:windbg 或 Visual Studio
- 使用windbg(微软调试工具)
🔹总结
仅需 Java 层到本地方法的入口:用jstack或jcmd即可,简单高效。
需要本地代码(C/C++)的详细调用链:使用操作系统工具(pstack、gdb、windbg),或在自定义 JNI 代码中添加堆栈打印逻辑。
解读线程堆栈 🔖
public class MyTest { Object obj1 = new Object(); Object obj2 = new Object(); public void fun1() { synchronized (obj1) { fun2(); } } public void fun2() { synchronized (obj2) { while (true) { System.out.println(""); } } } public static void main(String[] args) { MyTest aa = new MyTest(); aa.fun1(); } }



🔹本地线程和Java线程的映射

一个线程堆栈的例子 🔖
想象你在观察一个厨师(线程)工作:
- 他正在做一道 “番茄炒蛋”,步骤是:洗番茄→切番茄→打鸡蛋→翻炒→装盘。
- 如果你突然拍下一张照片(堆栈快照),照片上会显示:
- 厨师当前状态:正在 “切番茄”(对应线程状态);
- 做这道菜的完整步骤链条:从 “开始做菜” 到 “洗番茄” 再到 “切番茄”(对应方法调用链);
- 现在具体做到哪一步(对应代码行号)。
我们用一个简单的 Java 程序模拟线程工作,然后通过jstack获取它的堆栈信息:
public class CookingThread extends Thread { // 模拟做菜的一系列方法(函数) public void startCooking() { washTomato(); // 第一步:洗番茄 } public void washTomato() { System.out.println("洗番茄"); cutTomato(); // 第二步:切番茄 } public void cutTomato() { System.out.println("切番茄"); try { Thread.sleep(5000); // 切番茄时停顿5秒(方便我们抓取堆栈) } catch (InterruptedException e) { e.printStackTrace(); } cook(); // 第三步:烹饪 } public void cook() { System.out.println("翻炒中..."); } @Override public void run() { startCooking(); // 线程入口:开始做菜 } public static void main(String[] args) { new CookingThread().start(); // 启动线程 } }
线程堆栈信息(用jstack获取): 当程序运行到cutTomato()方法的sleep时(正在 “切番茄”),执行jstack <进程ID>,会得到类似以下的堆栈信息:
"CookingThread" #10 prio=5 os_prio=31 tid=0x00007f8a1b008000 nid=0x5a03 waiting on condition [0x000070000f28d000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) // 正在执行sleep(对应“切番茄时停顿”) at CookingThread.cutTomato(CookingThread.java:18) // 在cutTomato方法,第18行 at CookingThread.washTomato(CookingThread.java:10) // 被washTomato调用 at CookingThread.startCooking(CookingThread.java:6) // 被startCooking调用 at CookingThread.run(CookingThread.java:28) // 线程入口run方法调用 at java.lang.Thread.run(Thread.java:748) // 底层Thread类的run方法
🔹 堆栈信息详解(对应场景)
- 线程基本信息:
"CookingThread":线程名(我们创建的厨师名字); TIMED_WAITING (sleeping):线程当前状态(正在 “切番茄时停顿”,定时等待)。
- 方法调用链(核心):
最上层at java.lang.Thread.sleep(...):当前正在执行的操作(正在停顿); 下一行at CookingThread.cutTomato(...):当前在cutTomato方法(正在 “切番茄”); 再下一行at CookingThread.washTomato(...):cutTomato是被washTomato调用的(先 “洗番茄” 再 “切番茄”); 以此类推,直到最底层的Thread.run():线程的执行起点。
- 代码位置:
CookingThread.java:18:明确指出当前操作在代码的第 18 行(即sleep那一行),直接定位到具体代码。
线程死锁的例子 🔖
当程序出现问题(比如卡着不动、响应慢)时,光看代码很难定位原因。线程堆栈能告诉你:
举个通俗的例子:假设我们有一个简单的 Java 程序,模拟两个线程抢锁的场景:
public class ThreadStackDemo { // 定义两个锁对象 static Object lockA = new Object(); static Object lockB = new Object(); public static void main(String[] args) { // 线程1:先抢lockA,再抢lockB new Thread(() -> { synchronized (lockA) { System.out.println("线程1拿到了lockA,准备拿lockB"); try { Thread.sleep(1000); } catch (InterruptedException e) {} // 故意等一下,让线程2有机会抢锁 synchronized (lockB) { System.out.println("线程1拿到了lockB"); } } }, "线程1").start(); // 线程2:先抢lockB,再抢lockA new Thread(() -> { synchronized (lockB) { System.out.println("线程2拿到了lockB,准备拿lockA"); try { Thread.sleep(1000); } catch (InterruptedException e) {} synchronized (lockA) { System.out.println("线程2拿到了lockA"); } } }, "线程2").start(); } }
在终端执行命令,编译运行java类。
javac ThreadStackDemo.java java ThreadStackDemo
运行后,可以看到下面控制台输出,程序会卡住不动(因为发生了死锁:线程 1 拿着 lockA 等 lockB,线程 2 拿着 lockB 等 lockA)。这时用 jstack
命令打印线程堆栈,就能看到问题所在。
线程1拿到了lockA,准备拿lockB 线程2拿到了lockB,准备拿lockA
在终端执行下面命令拿到 Java 的pid
ps -ef | grep java

用jstack 导出java 堆栈信息
jstack 47206
通过jstack dump的文件,可以看到有两个线程block了
Virtual machine: 47206 JVM information: JRE 17 Mac OS X aarch64-64-Bit 20221018_182 (JIT enabled, AOT enabled) OpenJ9 - e04a7f6c1 OMR - 85a21674f JCL - 32d2c409a33 based on jdk-17.0.5+8 "JIT Compilation Thread-000" prio=10 Id=3 RUNNABLE "JIT Compilation Thread-001 Suspended" prio=10 Id=4 RUNNABLE "JIT Compilation Thread-002 Suspended" prio=10 Id=5 RUNNABLE "JIT Compilation Thread-003 Suspended" prio=10 Id=6 RUNNABLE "JIT Compilation Thread-004 Suspended" prio=10 Id=7 RUNNABLE "JIT Compilation Thread-005 Suspended" prio=10 Id=8 RUNNABLE "JIT Compilation Thread-006 Suspended" prio=10 Id=9 RUNNABLE "JIT Diagnostic Compilation Thread-007 Suspended" prio=10 Id=10 RUNNABLE "JIT-SamplerThread" prio=10 Id=11 TIMED_WAITING "IProfiler" prio=5 Id=12 RUNNABLE "Common-Cleaner" prio=8 Id=2 TIMED_WAITING at java.base@17.0.5/java.lang.Object.wait(Native Method) at java.base@17.0.5/java.lang.Object.wait(Object.java:219) at java.base@17.0.5/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:137) at java.base@17.0.5/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140) at java.base@17.0.5/java.lang.Thread.run(Thread.java:857) at java.base@17.0.5/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:162) "Concurrent Mark Helper" prio=1 Id=13 RUNNABLE "GC Worker" prio=5 Id=14 RUNNABLE "GC Worker" prio=5 Id=15 RUNNABLE "GC Worker" prio=5 Id=16 RUNNABLE "GC Worker" prio=5 Id=17 RUNNABLE "GC Worker" prio=5 Id=18 RUNNABLE "GC Worker" prio=5 Id=19 RUNNABLE "GC Worker" prio=5 Id=20 RUNNABLE "Attach API wait loop" prio=10 Id=23 TIMED_WAITING at java.base@17.0.5/java.lang.Thread.sleepImpl(Native Method) at java.base@17.0.5/java.lang.Thread.sleep(Thread.java:1009) at java.base@17.0.5/java.lang.Thread.sleep(Thread.java:992) at java.base@17.0.5/openj9.internal.tools.attach.target.WaitLoop.checkReplyAndCreateAttachment(WaitLoop.java:142) at java.base@17.0.5/openj9.internal.tools.attach.target.WaitLoop.waitForNotification(WaitLoop.java:117) at java.base@17.0.5/openj9.internal.tools.attach.target.WaitLoop.run(WaitLoop.java:157) "Finalizer thread" prio=5 Id=24 RUNNABLE "线程1" prio=5 Id=25 BLOCKED on java.lang.Object@979c7710 owned by "线程2" Id=26 at app//ThreadStackDemo.lambda$main$0(ThreadStackDemo.java:12) - locked java.lang.Object@db75ba2 at java.base@17.0.5/java.lang.Thread.run(Thread.java:857) "线程2" prio=5 Id=26 BLOCKED on java.lang.Object@db75ba2 owned by "线程1" Id=25 at app//ThreadStackDemo.lambda$main$1(ThreadStackDemo.java:23) - locked java.lang.Object@979c7710 at java.base@17.0.5/java.lang.Thread.run(Thread.java:857) "DestroyJavaVM helper thread" prio=5 Id=27 RUNNABLE "Attachment portNumber: 63972" prio=10 Id=28 RUNNABLE at java.base@17.0.5/openj9.internal.tools.attach.target.DiagnosticUtils.dumpAllThreadsImpl(Native Method) at java.base@17.0.5/openj9.internal.tools.attach.target.DiagnosticUtils.getThreadInfo(DiagnosticUtils.java:245) at java.base@17.0.5/openj9.internal.tools.attach.target.DiagnosticUtils.executeDiagnosticCommand(DiagnosticUtils.java:181) at java.base@17.0.5/openj9.internal.tools.attach.target.Attachment.doCommand(Attachment.java:248) at java.base@17.0.5/openj9.internal.tools.attach.target.Attachment.run(Attachment.java:159) "file lock watchdog" prio=10 Id=29 TIMED_WAITING at java.base@17.0.5/java.lang.Object.wait(Native Method) at java.base@17.0.5/java.lang.Object.wait(Object.java:219) at java.base@17.0.5/java.util.TimerThread.mainLoop(Timer.java:569) at java.base@17.0.5/java.util.TimerThread.run(Timer.java:522)
🔹虚拟机基本信息
- JVM 版本:OpenJ9 虚拟机(而非 HotSpot),JRE 17,运行在 Mac OS X aarch64(苹果 M 系列芯片)架构上。
- 特性:启用了 JIT(即时编译)和 AOT(提前编译),这是 OpenJ9 的典型配置,侧重性能优化。
🔹线程分类与状态解读
◾JIT 编译相关线程(OpenJ9 核心线程)
- JIT Compilation Thread-000至006(Id=3~9,优先级 10):
负责 JIT 编译(将字节码动态编译为机器码)的工作线程。部分标记为Suspended(暂停),是正常现象 ——JIT 线程会根据代码热点动态激活 / 暂停,并非异常。 - JIT Diagnostic Compilation Thread-007(Id=10,优先级 10):
用于 JIT 编译的诊断线程,可能用于记录编译日志或调试信息,状态Suspended表示当前无诊断任务。 - JIT-SamplerThread(Id=11,优先级 10,状态TIMED_WAITING):
JIT 采样线程,定期采样代码执行热点,辅助 JIT 决定哪些代码需要编译优化,通过定时等待(TIMED_WAITING)减少资源消耗。 - 总结:JIT 相关线程状态正常,体现了 OpenJ9 对即时编译的动态管理。
◾垃圾回收(GC)相关线程
- Concurrent Mark Helper(Id=13,优先级 1):
GC 并发标记阶段的辅助线程,状态RUNNABLE表示当前可能在进行并发标记(GC 的一个阶段,不阻塞用户线程)。 - GC Worker(Id=14~20,优先级 5,状态RUNNABLE):
共 8 个 GC 工作线程,用于执行垃圾回收的具体操作(如对象标记、清理)。多线程并行工作说明当前使用的是并行 GC 收集器(符合 OpenJ9 的默认配置),RUNNABLE状态表示 GC 正在进行或准备中,属于正常的内存管理行为。
◾用户自定义线程(核心问题)
- 线程1(Id=25,优先级 5,状态BLOCKED):
- 阻塞原因:等待锁java.lang.Object@979c7710,但该锁被线程2(Id=26)持有。
- 已持有锁:java.lang.Object@db75ba2(通过- locked标记)。
- 代码位置:卡在ThreadStackDemo.java:12行。
- 线程2(Id=26,优先级 5,状态BLOCKED):
- 阻塞原因:等待锁java.lang.Object@db75ba2,但该锁被线程1(Id=25)持有。已持有锁:java.lang.Object@979c7710。代码位置:卡在ThreadStackDemo.java:23行。

🔹启动 jconsole,选择目标 Java 进程连接


🔹 总结
一个 Java 程序运行时,包含的线程数量并不是固定的,而是由程序本身的代码、Java 虚拟机(JVM)的内部机制以及所使用的类库共同决定的。其中至少包含多个 “默认线程”,再加上程序自己创建的线程。
即使是一个最简单的 Java 程序(比如只打印一行文字就结束),运行时也会有多个线程在工作。这些线程是 JVM 自动创建的,用于支撑程序的基本运行,常见的包括:主线程(main 线程),GC 线程(垃圾回收线程),其他 JVM 内部线程。通过这段堆栈,我们能准确定位死锁根源,同时确认 JVM 自身运行状态正常,问题集中在用户线程的锁竞争逻辑上。线程堆栈就像程序的 “体检报告”,能直观展示每个线程的 “动作” 和 “状态”。通过分析它,我们能快速找到死锁、线程阻塞、无限循环等问题的根源。实际开发中,jstack
是排查线程问题的常用工具,一定要掌握哦!