JAVA

Java线程堆栈分析🌀

 

什么是 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 是排查线程问题的常用工具,一定要掌握哦!