首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

jstack 實戰指南:精準定位死循環、線程阻塞、死鎖與 I/O 等待

     編輯:Java 綜合教學

前言

當 Java 應用出現「卡死」、「響應極慢」、「CPU 飆高」等疑難雜症時,JDK 自帶的 jstack 是首選現場診斷工具。本文通過四大經典故障場景實戰,演示如何利用線程快照秒級定位問題代碼行,掌握生產環境故障排查核心技能。

一、工具鏈準備

1. 獲取進程 PID

# 方式一:jps(推薦,顯示主類名)
jps -l
# 7588 concurrency.Test

# 方式二:tasklist | findstr java(Windows)
tasklist | findstr java
# javaw.exe  7588 Console  1  123,456 K

2. 生成線程快照

jstack 7588 > threaddump.txt
# 或直接控制台查看
jstack 7588

進階參數:-l 顯示鎖信息、-m 混合模式(含 Native 堆棧)、-F 強制導出(進程掛起時)

二、四大經典故障場景實戰

場景 1:死循環導致 CPU 100%

故障代碼

package concurrency;
public class Test {
    public static void main(String[] args) throws InterruptedException {
        while (true) { } // 第 8 行
    }
}

線程快照關鍵信息

"main" #1 prio=5 os_prio=0 tid=0x0000000002a9b000 nid=0x1da4 runnable [0x0000000002b9e000]
   java.lang.Thread.State: RUNNABLE
    at concurrency.Test.main(Test.java:8)

診斷結論

  • 線程狀態:RUNNABLE(佔用 CPU 執行)
  • 堆棧定位:Test.java:8 正是 while(true) 行
  • 排查方向:檢查循環終止條件、是否缺少 sleep/yield、業務邏輯是否預期長時間運行

場景 2:Object.wait() 導致線程無限等待

故障代碼

class TestTask implements Runnable {
    public void run() {
        synchronized (this) {
            try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Executors.newFixedThreadPool(1).execute(new TestTask());
    }
}

線程快照關鍵信息

"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x000000001e2c3800 nid=0x1f4c in Object.wait() [0x000000001f5fe000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at concurrency.TestTask.run(Test.java:13)
    - locked <0x00000000d5f8a1b0> (a concurrency.TestTask)

診斷結論

  • 線程狀態:WAITING (on object monitor)
  • 堆棧定位:Test.java:13 即 wait() 調用行
  • 鎖對象:<0x00000000d5f8a1b0> 為 TestTask 實例
  • 排查方向:確認是否有對應 notify/notifyAll、鎖對象是否一致、是否應使用 wait(timeout) 避免無限等待

場景 3:經典死鎖

故障代碼

class TestTask implements Runnable {
    private Object obj1, obj2;
    private int order; // 0 或 1,控制加鎖順序相反
    
    public void test1() { synchronized(obj1) { Thread.yield(); synchronized(obj2) { ... } } }
    public void test2() { synchronized(obj2) { Thread.yield(); synchronized(obj1) { ... } } }
    
    public void run() {
        while(true) { if(order==1) test1(); else test2(); }
    }
}
public class Test {
    public static void main(String[] args) {
        Object obj1 = new Object(), obj2 = new Object();
        ExecutorService ex = Executors.newFixedThreadPool(10);
        for(int i=0;i<10;i++) ex.execute(new TestTask(i%2==0?1:0, obj1, obj2));
    }
}

線程快照關鍵信息(jstack 自動檢測)

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000002b3c3e8 (object 0x00000000d5f8a220, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000002b3c438 (object 0x00000000d5f8a250, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
    at concurrency.TestTask.test2(Test.java:45)
    - waiting to lock <0x00000000d5f8a220> (a java.lang.Object)
    - locked <0x00000000d5f8a250> (a java.lang.Object)
    at concurrency.TestTask.run(Test.java:58)
"Thread-0":
    at concurrency.TestTask.test1(Test.java:35)
    - waiting to lock <0x00000000d5f8a250> (a java.lang.Object)
    - locked <0x00000000d5f8a220> (a java.lang.Object)
    at concurrency.TestTask.run(Test.java:58)

Found 1 deadlock.

診斷結論

  • jstack 直接報告:Found 1 deadlock
  • 兩線程互相持有對方需要的鎖(obj1、obj2),形成循環等待
  • 代碼定位:TestTask.test1() 第 35 行、test2() 第 45 行
  • 根因:多線程以不同順序獲取同一組鎖
  • 修復:統一全局鎖獲取順序、使用 ReentrantLock.tryLock(timeout)、縮小同步塊範圍

場景 4:I/O 讀取阻塞

故障代碼

public class Test {
    public static void main(String[] args) throws IOException {
        InputStream is = System.in;
        int i = is.read(); // 第 12 行,等待用戶輸入
        System.out.println("exit。");
    }
}

線程快照關鍵信息

"main" #1 prio=5 os_prio=0 tid=0x0000000002a9b000 nid=0x1da4 runnable [0x0000000002b9e000]
   java.lang.Thread.State: RUNNABLE
    at java.io.FileInputStream.readBytes(Native Method)
    at java.io.FileInputStream.read(FileInputStream.java:255)
    at java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
    at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
    at java.io.FilterInputStream.read(FilterInputStream.java:107)
    at concurrency.Test.main(Test.java:12)

診斷結論

  • 線程狀態:RUNNABLE(注意:I/O 讀取在 JVM 層面顯示為 RUNNABLE,實際 OS 線程處於等待態)
  • 堆棧定位:Test.java:12 即 System.in.read()
  • Native 方法:FileInputStream.readBytes 表明進入 OS 系統調用
  • 排查方向:網絡讀取(Socket.read)、文件讀取、控制台輸入等無超時設置導致無限阻塞

三、其他常見阻塞狀態速查表

線程狀態典型代碼堆棧特徵排查方向
BLOCKEDsynchronized 進入阻塞waiting for monitor entry鎖競爭激烈、長耗時同步塊
WAITINGObject.wait()、Thread.join()、LockSupport.park()in Object.wait()、parking缺少 notify、超時設置、死鎖
TIMED_WAITINGThread.sleep()、wait(timeout)、join(timeout)、parkNanossleeping、waiting with timeout業務邏輯正常休眠、超時保護
RUNNABLE (I/O)Socket.read、FileInputStream.read、System.in.readNative Method、readBytes設置 SO_TIMEOUT、NIO 非阻塞、異步 I/O

四、生產環境排查最佳實踐

  1. 多次抽樣:間隔 10-20 秒連續執行 3-5 次 jstack,對比堆棧變化,區分「瞬間慢」與「持續卡住」
  2. 關注 RUNNABLE:CPU 高時重點分析 RUNNABLE 線程,排除 GC 線程(名稱含 GC、Compiler、Finalizer)
  3. 鎖分析加 -l:jstack -l pid 顯示 java.util.concurrent.Lock 等顯式鎖信息
  4. 結合 top -H -p pid:Linux 下將高 CPU 線程 ID 轉十六進制(printf '%x\n' tid),與 jstack 中 nid=0x... 對應,精準定位「燒 CPU」代碼
  5. 保留現場:故障發生時先導出快照再重啟,避免現場丟失

五、結語

jstack 是 Java 開發者的「聽診器」。掌握四大經典場景(死循環、無限等待、死鎖、I/O 阻塞)的堆棧特徵識別,配合多次抽樣、CPU 線程對應、鎖信息增強等實戰技巧,能將故障定位時間從「小時級」壓縮到「分鐘級」。建議每位後端工程師熟練掌握線程快照閱讀能力,將其納入日常技能體系,從「會用框架」進階到「懂運行機制」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved