thread dump日誌文件分析

       在介紹thread dump文件之前,咱們得先來了解下Java線程狀態。一般來說Java線程狀態有五種:新建狀態(New)、就緒狀態(Runnable)、運行狀態(Running) 、阻塞狀態(Blocked) 、死亡狀態(Dead) 。

在這裏插入圖片描述

  • 新建狀態(New)

       用new語句創建的線程處於新建狀態,此時它和其他Java對象一樣,僅僅在堆區中被分配了內存。(對象在堆區產生)

  • 就緒狀態(Runnable)

       當一個線程對象創建後,其他線程調用它的start()方法,該線程就進入就緒狀態,Java虛擬機會爲它創建方法調用棧和程序計數器。處於這個狀態的線程位於可運行池中,等待獲得CPU的使用權。

  • 運行狀態(Running)

       處於這個狀態的線程佔用CPU,執行程序代碼。只有處於就緒狀態的線程纔有機會轉到運行狀態。

  • 阻塞狀態(Blocked)

       阻塞狀態是指線程因爲某些原因放棄CPU,暫時停止運行。當線程處於阻塞狀態時,Java虛擬機不會給線程分配CPU。直到線程重新進入就緒狀態,它纔有機會轉到運行狀態。阻塞狀態也是分多鐘情況的:等待阻塞、同步阻塞、其他阻塞。

  1. 等待阻塞:處於對象等待池中的阻塞狀態(Blocked in object’s wait pool): 當線程處於運行狀態時,如果執行了某個對象的wait()方法,Java虛擬機就會把線程放到這個對象的等待池中,這涉及到“線程通信”的內容。(對應wait()方法)
  2. 同步阻塞:處於對象鎖池中的阻塞狀態(Blocked in object’s lock pool): 當線程處於運行狀態時,試圖獲得某個對象的同步鎖時,如果該對象的同步鎖已經被其他線程佔用,Java虛擬機就會把這個線程放到這個對象的鎖池中,這涉及到“線程同步”的內容。(對應synchronized同步鎖)
  3. 其他阻塞:處於其他阻塞狀態(Ohterwise Blick): 當前線程執行了sleep()方法,或者調用了其他線程的join()方法,或者發出了I/O請求時,就會進入這個狀態。(對應sleep()或者join()或發出了I/O請求時)
  • 死亡狀態(Dead)

       當線程退出run()方法時,就進入死亡狀態,該線程結束生命週期。


       接下來,我們才進入到我們本文的重點,thread dump日誌文件

一 thread dump日誌文件介紹

1.1 thread dump日誌文件是什麼

       thread dump日誌文件是應用程序當前活動線程的快照(所有線程堆棧)信息,簡單來說thread dump日誌裏面保存的是某一時刻對應應用程序所有活動線程的詳細信息。而且thread dump日誌文件是一個純文本文件。

thread dump文件裏面爲每個線程提供了:線程名字,id、線程的運行狀態,鎖的狀態(鎖被哪個線程持有,哪個線程在等待鎖等)、調用堆棧信息(包含完整的類名,所執行的方法,源代碼的行數等)。

1.2 thread dump日誌文件的獲取

       我們一般使用JVM自帶的jps和jstack工具來獲取thread dump文件。先用jps命令獲取到應用程序的PID,獲取PID之後在通過jstack命令來導出對應的thread dump日誌文件。

jps -l 獲取PID(Linux系統下你也可以使用 ps –ef | grep java)。
jstack -l <PID> | tee -a 文件名字

       我用一個簡單的例子來說明下,比如我要獲取我電腦裏面basic-ioserver-websocket-server.jar程序對應的thread dump日誌文件信息。

  • jps -l 找到basic-ioserver-websocket-server.jar對應的PID 51164

在這裏插入圖片描述

  • jstack -l 51164 >> ioserver-websocket-thread-dump20200328.txt 把thread dump文件保存在ioserver-websocket-thread-dump20200328.txt文件裏面

1.3 thread dump日誌文件解析

       上面我們已經知道了怎麼獲取thread dump日誌文件。接下來我們得知道怎麼去分析thread dump日誌文件。thread dump日誌文件是一個純文本格式的文件。裏面給出了thread dump日誌文件的生成時間,以及JVM信息,還有當前時刻所有線程的詳細信息,每個線程的信息又包括:線程名字,線程編號,線程優先級,線程狀態,線程詳細狀態,線程堆棧信息等等。

       我們簡單一點,我直接用一個圖來表示thread dump日誌文件的格式(每個信息所處的位置),如下所示:

在這裏插入圖片描述

       關於thread dump日誌文件重要關注點在:線程名字(用於找到對應的線程),線程狀態,線程詳細狀態,線程堆棧信息。

1.3.1 thread dump日誌線程狀態

       thread dump日誌文件裏面的線程狀態和咱們最上面講的Java中的線程狀態還不太一樣。理解上可能會出現混亂。其實咱們也不用太糾結。因爲雖然Java中的線程狀態有五種,但是大多時候我們關注的是運行狀態和阻塞狀態,線程新建狀態和死亡狀態持續時間都是很短的。我們也不用太關心。thread dump線程狀態如下:

thread dump線程狀態 解釋
runnable 線程運行中或I/O等待
sleeping 當前線程被掛起
waiting on condition 在等待另一個條件的發生,來把自己喚醒
waiting for monitor entry 在等待進入一個臨界區,所以它在”Entry Set“隊列中等待
in Object.wait() 進入臨界區支行,又調用了java.lang.Object.wait()方法等待,在“Wait Set”對列中等待

monitor是Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象或者Class的鎖。每一個對象都有,也僅有一個monitor。每個monitor在某個時刻,只能被一個線程擁有,該線程就是“Active Thread”,而其它線程都是“Waiting Thread”,分別在兩個隊列“Entry Set”和 “Wait Set”裏面等候。在“Entry Set”中等待的線程狀態是“Waiting for monitor entry”,而在“Wait Set”中等待的線程狀態是“in Object.wait()”。在“Entry Set”中的是試圖獲取鎖而阻塞的線程;而“Wait Set”則是,獲取鎖後調用wait()而進入阻塞的線程,當其他線程調用notify()或者notifyall()喚醒等待該鎖上的其他線程時,會把“Wait Set”隊列中的一個或者全部線程放入“Entry Set”中,然後在其中隨機選擇一個重新獲取鎖,然後執行。

1.3.2 thread dump日誌線程狀態詳細信息

       通過查看線程狀態的詳細信息,我們可以簡單的判斷我們線程是因爲啥進入了對應的線程狀態。

thread dump線程狀態詳細信息 解釋 對應的方法調用
java.lang.Thread.State: RUNNABLE 線程運行中或I/O等待
java.lang.Thread.State: BLOCKED (on object monitor) 等待進入一個臨界區 synchronized
java.lang.Thread.State: TIMED_WAITING (parking) 線程等待喚醒,並且設置等待時長 LockSupport.parkNanos(等待時長)、LockSupport.parkUntil(等待時長)
java.lang.Thread.State: TIMED_WAITING (sleeping) 線程等待喚醒,並且設置等待時長 Thread.sleep(等待時長),Thread.join(等待時長)
java.lang.Thread.State: TIMED_WAITING (on object monitor) 線程在等待喚醒,並且設置等待時長 Object.wait(等待時長)
java.lang.Thread.State: WAITING (parking) 線程等待喚醒,沒有設置等待時長 LockSupport.park()
java.lang.Thread.State: WAITING (on object monitor) 線程等待喚醒,並且沒有設置等待時長 Object.wait()
java.lang.Thread.State: WAITING (on object monitor) 線程等待喚醒,沒有設置等待時長 Thread.join()

1.3.3 thread dump日誌文件線程狀態實例解釋

       下面我們通過幾個簡單的實例來理解下thread dump線程狀態

  • wait(),sleep()對應的線程狀態
    public static void main(String[] args) {

        Thread thread = new Thread("線程1") {
            //重寫run方法
            public void run() {
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (thread) {
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread.notify();
        }


    }

       上面代碼會先執行線程1 run()方法,然後調用wait()方法阻塞block住。等到主線程調用thread.notify()方法之後纔會繼續往下執行。我們在程序跑起來之後大概10秒時候導出thread dump日誌文件信息,此時:

  • [線程1]線程:wait()方法阻塞住了,狀態對應in Object.wait(),狀態詳細信息對應java.lang.Thread.State: WAITING (on object monitor)。
  • [main]線程:TimeUnit.SECONDS.sleep(60)阻塞住了,狀態對應waiting on condition,狀態詳細信息對應java.lang.Thread.State: TIMED_WAITING (sleeping)。

對應thread dump日誌文件的部分信息

2020-03-23 22:54:53
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):

"線程1" #12 prio=5 os_prio=0 tid=0x00007f420024d800 nid=0x1ca5 in Object.wait() [0x00007f41e71ee000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000d8258ba0> (a com.jvm.study.threaddump.deadlock.ObjectWaitingMock$1)
	at java.lang.Object.wait(Object.java:502)
	at com.jvm.study.threaddump.deadlock.ObjectWaitingMock$1.run(ObjectWaitingMock.java:15)
	- locked <0x00000000d8258ba0> (a com.jvm.study.threaddump.deadlock.ObjectWaitingMock$1)

   Locked ownable synchronizers:
	- None

....(其他信息這裏我們省略掉)

"main" #1 prio=5 os_prio=0 tid=0x00007f420000d800 nid=0x1c84 waiting on condition [0x00007f4209891000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.jvm.study.threaddump.deadlock.ObjectWaitingMock.main(ObjectWaitingMock.java:31)
	- locked <0x00000000d8258ba0> (a com.jvm.study.threaddump.deadlock.ObjectWaitingMock$1)

   Locked ownable synchronizers:
	- None

...(其他信息這裏我們省略掉)
  • synchronized對應線程狀態
    public static void main(String[] args) {

        Thread thread = new Thread("線程") {
            //重寫run方法
            public void run() {
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        TimeUnit.SECONDS.sleep(60);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();

        synchronized (thread) {
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

       程序剛開始執行的時候會先進入主線程的synchronized然後進入TimeUnit.SECONDS.sleep(60)阻塞,Thread(“線程”)因爲主線程還阻塞主synchronized對應的鎖還沒有放開,所有會阻塞住

  • [線程]線程:synchronized等待進入同步狀態。狀態對應waiting for monitor entry,狀態詳細信息對應java.lang.Thread.State: BLOCKED (on object monitor)
  • [main]線程:TimeUnit.SECONDS.sleep(60)。狀態對應waiting on condition,狀態詳細信息對應java.lang.Thread.State: TIMED_WAITING (sleeping)

對thread dump日誌文件信息

2020-03-23 22:34:06
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):

...省略掉其他信息

"線程" #12 prio=5 os_prio=0 tid=0x00007f7ccc24d000 nid=0x195f waiting for monitor entry [0x00007f7caa8e5000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.jvm.study.threaddump.deadlock.BlockMock$1.run(BlockMock.java:16)
	- waiting to lock <0x00000000d8258140> (a com.jvm.study.threaddump.deadlock.BlockMock$1)

   Locked ownable synchronizers:
	- None

...省略掉其他信息

"main" #1 prio=5 os_prio=0 tid=0x00007f7ccc00d800 nid=0x193a waiting on condition [0x00007f7cd4f00000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.jvm.study.threaddump.deadlock.BlockMock.main(BlockMock.java:30)
	- locked <0x00000000d8258140> (a com.jvm.study.threaddump.deadlock.BlockMock$1)

   Locked ownable synchronizers:
	- None

二 thread dump文件能做什麼(能幫咱們解決什麼問題)

       我們瞭解thread dump日誌文件的最終肯定是用他來幫我們解決問題的,咱們不可能無緣無故的去導出thread dump日誌文件而什麼都不幹吧。

       在用thread dump文件解決問題之前,咱們得先找個好點的thread dump日誌文件的分析工具。工欲善其事必先利其器。

2.1 thread dump日誌文件分析工具

2.1.1 fastthread在線工具

       fastthread在線工具,在線網址 http://fastthread.io/

       打開網址,在網頁上選擇我們需要分析的thread dump文件。通過fastthread工具。我們可以很容易的知道線程統計信息(WAITING、RUNNABLE、TIMED_WAITING、BLOCK中的線程個數,以及每種狀態裏面的所有線程)、守護程序與非守護程序的統計信息、具有相同堆棧跟蹤的線程信息、最常用的方法(park()、wait()、sleep、)、自底向上調用堆棧樹等等一系列的信息。很方便的來分類定位信息。

在這裏插入圖片描述

2.1.2 IBM Thread and Monitor Dump Analyzer for Java

       通過網址 https://www.ibm.com/support/pages/ibm-thread-and-monitor-dump-analyzer-java-tmda 我們可以下載到jca465.jar(也可能是jcaxxx.jar啥的,不一定是jca465.jar)。下載到jca465.jar文件之後我們就好辦了。

       執行jca465.jar文件。 java -jar jca465.jar 執行之後,會打開相應的圖形化界面。之後選擇我們需要分析的thread dump文件。裏面有展示線程的統計信息,線程的詳細信息等,使用起來都很方便。

在這裏插入圖片描述
在這裏插入圖片描述

2.2 thread dump案例分析

2.2.1 CPU佔用率很高,響應很慢

       先找到佔用CPU的進程,然後再定位到對應的線程,最後分析出對應的堆棧信息。所以我們得關鍵點在獲取到佔CPU線程的堆棧信息。需要在多個時間段提出多個 Thread Dump信息,然後綜合進行對比分析,單獨分析一個文件是沒有意義的。

  • top -c 動態顯示進程及佔用資源的排行榜)找到佔用CPU最高的進程PID
  • 通過PID去找到當前PID下佔CPU最高的線程的堆棧信息。關於這一部分的內容網上以及有高手幫我們寫成了一個腳本(Linux環境),我們只需要執行腳本,執行腳本的時候需要輸入兩個參數第一個參數對應PID,第二個參數對應需要顯示佔CPU線程堆棧的多少行數據。腳本文件如下:
#!/bin/bash
if [ $# -le 0 ]; then
    echo "usage: $0 <pid> [line-number]"
    exit 1
fi

# java home
if test -z $JAVA_HOME 
then
    JAVA_HOME='/usr/local/jdk'
fi

#pid -- 第一個參數進程pid
pid=$1
# checking pid
if test -z "$($JAVA_HOME/bin/jps -l | cut -d '' -f 1 | grep $pid)"
then
    echo "process of $pid is not exists"
    exit
fi

#line number -- 第二個參數顯示多少行的堆棧信息
linenum=$2
if test -z $linenum
then
    linenum=10
fi

stackfile=stack$pid.dump
threadsfile=threads$pid.dump

# generate java stack
$JAVA_HOME/bin/jstack -l $pid >> $stackfile
ps -mp $pid -o THREAD,tid,time | sort -k2r | awk '{if ($1 !="USER" && $2 != "0.0" && $8 !="-") print $8;}' | xargs printf "%x\n" >> $threadsfile
tids="$(cat $threadsfile)"
for tid in $tids
do
    echo "------------------------------ ThreadId ($tid) ------------------------------"
    cat $stackfile | grep 0x$tid -A $linenum
done

rm -f $stackfile $threadsfile

比如我們想看下PID=82125裏面最佔CPU線程的前10行堆棧信息(sh height-thread-cpu-stack.sh 82125 10),最後顯示的效果如下:

在這裏插入圖片描述

2.2.2 CPU佔用率不高,但響應很慢

       在整個請求的過程中多次執行Thread Dump然後進行對比,取得 BLOCKED狀態的線程列表(通過我們上面提供的thread dump工具是很容易獲取到的),通常是因爲線程停在了I/O、數據庫連接或網絡連接的地方。

2.2.3 線程死鎖

       線程死鎖,thread dump文件裏面肯定有給出Found one Java-level deadlock:信息。

2020-03-22 23:04:49
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):


"線程2" #13 prio=5 os_prio=0 tid=0x00007f1d3825f800 nid=0x2142 waiting for monitor entry [0x00007f1d16eeb000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.jvm.study.threaddump.deadlock.DeadLockMock$2.run(DeadLockMock.java:31)
	- waiting to lock <0x00000000d8251168> (a java.lang.String)
	- locked <0x00000000d8251198> (a java.lang.String)

   Locked ownable synchronizers:
	- None

"線程1" #12 prio=5 os_prio=0 tid=0x00007f1d3825e000 nid=0x2141 waiting for monitor entry [0x00007f1d16fec000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.jvm.study.threaddump.deadlock.DeadLockMock$1.run(DeadLockMock.java:16)
	- waiting to lock <0x00000000d8251198> (a java.lang.String)
	- locked <0x00000000d8251168> (a java.lang.String)

   Locked ownable synchronizers:
	- None


Found one Java-level deadlock:
=============================
"線程2":
  waiting to lock monitor 0x00007f1d04006218 (object 0x00000000d8251168, a java.lang.String),
  which is held by "線程1"
"線程1":
  waiting to lock monitor 0x00007f1d04002178 (object 0x00000000d8251198, a java.lang.String),
  which is held by "線程2"

Java stack information for the threads listed above:
===================================================
"線程2":
	at com.jvm.study.threaddump.deadlock.DeadLockMock$2.run(DeadLockMock.java:31)
	- waiting to lock <0x00000000d8251168> (a java.lang.String)
	- locked <0x00000000d8251198> (a java.lang.String)
"線程1":
	at com.jvm.study.threaddump.deadlock.DeadLockMock$1.run(DeadLockMock.java:16)
	- waiting to lock <0x00000000d8251198> (a java.lang.String)
	- locked <0x00000000d8251168> (a java.lang.String)

Found 1 deadlock.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章