? ? ? ?在介紹thread dump文件之前从绘,咱們得先來了解下Java線程狀態(tài)爪瓜。一般來說Java線程狀態(tài)有五種:新建狀態(tài)(New)桐汤、就緒狀態(tài)(Runnable)宗挥、運行狀態(tài)(Running) 、阻塞狀態(tài)(Blocked) 从诲、死亡狀態(tài)(Dead) 左痢。
- 新建狀態(tài)(New)
? ? ? ?用new語句創(chuàng)建的線程處于新建狀態(tài),此時它和其他Java對象一樣系洛,僅僅在堆區(qū)中被分配了內(nèi)存俊性。(對象在堆區(qū)產(chǎn)生)
- 就緒狀態(tài)(Runnable)
? ? ? ?當(dāng)一個線程對象創(chuàng)建后,其他線程調(diào)用它的start()方法描扯,該線程就進(jìn)入就緒狀態(tài)定页,Java虛擬機(jī)會為它創(chuàng)建方法調(diào)用棧和程序計數(shù)器。處于這個狀態(tài)的線程位于可運行池中绽诚,等待獲得CPU的使用權(quán)典徊。
- 運行狀態(tài)(Running)
? ? ? ?處于這個狀態(tài)的線程占用CPU,執(zhí)行程序代碼恩够。只有處于就緒狀態(tài)的線程才有機(jī)會轉(zhuǎn)到運行狀態(tài)卒落。
- 阻塞狀態(tài)(Blocked)
? ? ? ?阻塞狀態(tài)是指線程因為某些原因放棄CPU,暫時停止運行蜂桶。當(dāng)線程處于阻塞狀態(tài)時儡毕,Java虛擬機(jī)不會給線程分配CPU。直到線程重新進(jìn)入就緒狀態(tài)扑媚,它才有機(jī)會轉(zhuǎn)到運行狀態(tài)妥曲。阻塞狀態(tài)也是分多鐘情況的:等待阻塞、同步阻塞钦购、其他阻塞檐盟。
- 等待阻塞:處于對象等待池中的阻塞狀態(tài)(Blocked in object’s wait pool): 當(dāng)線程處于運行狀態(tài)時,如果執(zhí)行了某個對象的wait()方法押桃,Java虛擬機(jī)就會把線程放到這個對象的等待池中葵萎,這涉及到“線程通信”的內(nèi)容。(對應(yīng)wait()方法)
- 同步阻塞:處于對象鎖池中的阻塞狀態(tài)(Blocked in object’s lock pool): 當(dāng)線程處于運行狀態(tài)時,試圖獲得某個對象的同步鎖時羡忘,如果該對象的同步鎖已經(jīng)被其他線程占用谎痢,Java虛擬機(jī)就會把這個線程放到這個對象的鎖池中,這涉及到“線程同步”的內(nèi)容卷雕。(對應(yīng)synchronized同步鎖)
- 其他阻塞:處于其他阻塞狀態(tài)(Ohterwise Blick): 當(dāng)前線程執(zhí)行了sleep()方法节猿,或者調(diào)用了其他線程的join()方法,或者發(fā)出了I/O請求時漫雕,就會進(jìn)入這個狀態(tài)滨嘱。(對應(yīng)sleep()或者join()或發(fā)出了I/O請求時)
- 死亡狀態(tài)(Dead)
? ? ? ?當(dāng)線程退出run()方法時,就進(jìn)入死亡狀態(tài)浸间,該線程結(jié)束生命周期太雨。
? ? ? ?接下來,我們才進(jìn)入到我們本文的重點魁蒜,thread dump日志文件
一 thread dump日志文件介紹
1.1 thread dump日志文件是什么
? ? ? ?thread dump日志文件是應(yīng)用程序當(dāng)前活動線程的快照(所有線程堆棧)信息囊扳,簡單來說thread dump日志里面保存的是某一時刻對應(yīng)應(yīng)用程序所有活動線程的詳細(xì)信息。而且thread dump日志文件是一個純文本文件兜看。
thread dump文件里面為每個線程提供了:線程名字锥咸,id、線程的運行狀態(tài)细移,鎖的狀態(tài)(鎖被哪個線程持有搏予,哪個線程在等待鎖等)、調(diào)用堆棧信息(包含完整的類名葫哗,所執(zhí)行的方法,源代碼的行數(shù)等)球涛。
1.2 thread dump日志文件的獲取
? ? ? ?我們一般使用JVM自帶的jps和jstack工具來獲取thread dump文件劣针。先用jps命令獲取到應(yīng)用程序的PID,獲取PID之后在通過jstack命令來導(dǎo)出對應(yīng)的thread dump日志文件亿扁。
jps -l 獲取PID(Linux系統(tǒng)下你也可以使用 ps –ef | grep java)捺典。
jstack -l <PID> | tee -a 文件名字
? ? ? ?我用一個簡單的例子來說明下,比如我要獲取我電腦里面basic-ioserver-websocket-server.jar程序?qū)?yīng)的thread dump日志文件信息从祝。
- jps -l 找到basic-ioserver-websocket-server.jar對應(yīng)的PID 51164
- jstack -l 51164 >> ioserver-websocket-thread-dump20200328.txt 把thread dump文件保存在ioserver-websocket-thread-dump20200328.txt文件里面
1.3 thread dump日志文件解析
? ? ? ?上面我們已經(jīng)知道了怎么獲取thread dump日志文件襟己。接下來我們得知道怎么去分析thread dump日志文件。thread dump日志文件是一個純文本格式的文件牍陌。里面給出了thread dump日志文件的生成時間擎浴,以及JVM信息,還有當(dāng)前時刻所有線程的詳細(xì)信息毒涧,每個線程的信息又包括:線程名字贮预,線程編號,線程優(yōu)先級,線程狀態(tài)仿吞,線程詳細(xì)狀態(tài)滑频,線程堆棧信息等等。
? ? ? ?我們簡單一點唤冈,我直接用一個圖來表示thread dump日志文件的格式(每個信息所處的位置)峡迷,如下所示:
? ? ? ?關(guān)于thread dump日志文件重要關(guān)注點在:線程名字(用于找到對應(yīng)的線程),線程狀態(tài)你虹,線程詳細(xì)狀態(tài)绘搞,線程堆棧信息。
1.3.1 thread dump日志線程狀態(tài)
? ? ? ?thread dump日志文件里面的線程狀態(tài)和咱們最上面講的Java中的線程狀態(tài)還不太一樣售葡。理解上可能會出現(xiàn)混亂看杭。其實咱們也不用太糾結(jié)。因為雖然Java中的線程狀態(tài)有五種挟伙,但是大多時候我們關(guān)注的是運行狀態(tài)和阻塞狀態(tài)楼雹,線程新建狀態(tài)和死亡狀態(tài)持續(xù)時間都是很短的。我們也不用太關(guān)心尖阔。thread dump線程狀態(tài)如下:
thread dump線程狀態(tài) | 解釋 |
---|---|
runnable | 線程運行中或I/O等待 |
sleeping | 當(dāng)前線程被掛起 |
waiting on condition | 在等待另一個條件的發(fā)生贮缅,來把自己喚醒 |
waiting for monitor entry | 在等待進(jìn)入一個臨界區(qū),所以它在”Entry Set“隊列中等待 |
in Object.wait() | 進(jìn)入臨界區(qū)支行介却,又調(diào)用了java.lang.Object.wait()方法等待谴供,在“Wait Set”對列中等待 |
monitor是Java中用以實現(xiàn)線程之間的互斥與協(xié)作的主要手段,它可以看成是對象或者Class的鎖齿坷。每一個對象都有桂肌,也僅有一個monitor。每個monitor在某個時刻永淌,只能被一個線程擁有崎场,該線程就是“Active Thread”,而其它線程都是“Waiting Thread”遂蛀,分別在兩個隊列“Entry Set”和 “Wait Set”里面等候谭跨。在“Entry Set”中等待的線程狀態(tài)是“Waiting for monitor entry”,而在“Wait Set”中等待的線程狀態(tài)是“in Object.wait()”李滴。在“Entry Set”中的是試圖獲取鎖而阻塞的線程螃宙;而“Wait Set”則是,獲取鎖后調(diào)用wait()而進(jìn)入阻塞的線程所坯,當(dāng)其他線程調(diào)用notify()或者notifyall()喚醒等待該鎖上的其他線程時谆扎,會把“Wait Set”隊列中的一個或者全部線程放入“Entry Set”中,然后在其中隨機(jī)選擇一個重新獲取鎖芹助,然后執(zhí)行燕酷。
1.3.2 thread dump日志線程狀態(tài)詳細(xì)信息
? ? ? ?通過查看線程狀態(tài)的詳細(xì)信息籍凝,我們可以簡單的判斷我們線程是因為啥進(jìn)入了對應(yīng)的線程狀態(tài)。
thread dump線程狀態(tài)詳細(xì)信息 | 解釋 | 對應(yīng)的方法調(diào)用 |
---|---|---|
java.lang.Thread.State: RUNNABLE | 線程運行中或I/O等待 | 無 |
java.lang.Thread.State: BLOCKED (on object monitor) | 等待進(jìn)入一個臨界區(qū) | synchronized |
java.lang.Thread.State: TIMED_WAITING (parking) | 線程等待喚醒苗缩,并且設(shè)置等待時長 | LockSupport.parkNanos(等待時長)饵蒂、LockSupport.parkUntil(等待時長) |
java.lang.Thread.State: TIMED_WAITING (sleeping) | 線程等待喚醒,并且設(shè)置等待時長 | Thread.sleep(等待時長)酱讶,Thread.join(等待時長) |
java.lang.Thread.State: TIMED_WAITING (on object monitor) | 線程在等待喚醒退盯,并且設(shè)置等待時長 | Object.wait(等待時長) |
java.lang.Thread.State: WAITING (parking) | 線程等待喚醒,沒有設(shè)置等待時長 | LockSupport.park() |
java.lang.Thread.State: WAITING (on object monitor) | 線程等待喚醒泻肯,并且沒有設(shè)置等待時長 | Object.wait() |
java.lang.Thread.State: WAITING (on object monitor) | 線程等待喚醒渊迁,沒有設(shè)置等待時長 | Thread.join() |
1.3.3 thread dump日志文件線程狀態(tài)實例解釋
? ? ? ?下面我們通過幾個簡單的實例來理解下thread dump線程狀態(tài)
- wait(),sleep()對應(yīng)的線程狀態(tài)
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();
}
}
? ? ? ?上面代碼會先執(zhí)行線程1 run()方法灶挟,然后調(diào)用wait()方法阻塞block住琉朽。等到主線程調(diào)用thread.notify()方法之后才會繼續(xù)往下執(zhí)行。我們在程序跑起來之后大概10秒時候?qū)С鰐hread dump日志文件信息稚铣,此時:
- [線程1]線程:wait()方法阻塞住了箱叁,狀態(tài)對應(yīng)in Object.wait(),狀態(tài)詳細(xì)信息對應(yīng)java.lang.Thread.State: WAITING (on object monitor)惕医。
- [main]線程:TimeUnit.SECONDS.sleep(60)阻塞住了耕漱,狀態(tài)對應(yīng)waiting on condition,狀態(tài)詳細(xì)信息對應(yīng)java.lang.Thread.State: TIMED_WAITING (sleeping)抬伺。
對應(yīng)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對應(yīng)線程狀態(tài)
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();
}
}
}
? ? ? ?程序剛開始執(zhí)行的時候會先進(jìn)入主線程的synchronized然后進(jìn)入TimeUnit.SECONDS.sleep(60)阻塞螟够,Thread("線程")因為主線程還阻塞主synchronized對應(yīng)的鎖還沒有放開,所有會阻塞住
- [線程]線程:synchronized等待進(jìn)入同步狀態(tài)峡钓。狀態(tài)對應(yīng)waiting for monitor entry妓笙,狀態(tài)詳細(xì)信息對應(yīng)java.lang.Thread.State: BLOCKED (on object monitor)
- [main]線程:TimeUnit.SECONDS.sleep(60)。狀態(tài)對應(yīng)waiting on condition能岩,狀態(tài)詳細(xì)信息對應(yīng)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日志文件的最終肯定是用他來幫我們解決問題的寞宫,咱們不可能無緣無故的去導(dǎo)出thread dump日志文件而什么都不干吧。
? ? ? ?在用thread dump文件解決問題之前捧灰,咱們得先找個好點的thread dump日志文件的分析工具淆九。工欲善其事必先利其器统锤。
2.1 thread dump日志文件分析工具
2.1.1 fastthread在線工具
? ? ? ?fastthread在線工具毛俏,在線網(wǎng)址 http://fastthread.io/
? ? ? ?打開網(wǎng)址,在網(wǎng)頁上選擇我們需要分析的thread dump文件饲窿。通過fastthread工具煌寇。我們可以很容易的知道線程統(tǒng)計信息(WAITING、RUNNABLE逾雄、TIMED_WAITING阀溶、BLOCK中的線程個數(shù)腻脏,以及每種狀態(tài)里面的所有線程)、守護(hù)程序與非守護(hù)程序的統(tǒng)計信息银锻、具有相同堆棧跟蹤的線程信息永品、最常用的方法(park()、wait()击纬、sleep鼎姐、)、自底向上調(diào)用堆棧樹等等一系列的信息更振。很方便的來分類定位信息炕桨。
2.1.2 IBM Thread and Monitor Dump Analyzer for Java
? ? ? ?通過網(wǎng)址 https://www.ibm.com/support/pages/ibm-thread-and-monitor-dump-analyzer-java-tmda 我們可以下載到j(luò)ca465.jar(也可能是jcaxxx.jar啥的,不一定是jca465.jar)肯腕。下載到j(luò)ca465.jar文件之后我們就好辦了献宫。
? ? ? ?執(zhí)行jca465.jar文件。 java -jar jca465.jar 執(zhí)行之后实撒,會打開相應(yīng)的圖形化界面姊途。之后選擇我們需要分析的thread dump文件。里面有展示線程的統(tǒng)計信息奈惑,線程的詳細(xì)信息等吭净,使用起來都很方便。
2.2 thread dump案例分析
2.2.1 CPU占用率很高肴甸,響應(yīng)很慢
? ? ? ?先找到占用CPU的進(jìn)程寂殉,然后再定位到對應(yīng)的線程,最后分析出對應(yīng)的堆棧信息原在。所以我們得關(guān)鍵點在獲取到占CPU線程的堆棧信息友扰。需要在多個時間段提出多個 Thread Dump信息,然后綜合進(jìn)行對比分析庶柿,單獨分析一個文件是沒有意義的村怪。
- top -c 動態(tài)顯示進(jìn)程及占用資源的排行榜)找到占用CPU最高的進(jìn)程PID
- 通過PID去找到當(dāng)前PID下占CPU最高的線程的堆棧信息。關(guān)于這一部分的內(nèi)容網(wǎng)上以及有高手幫我們寫成了一個腳本(Linux環(huán)境)浮庐,我們只需要執(zhí)行腳本甚负,執(zhí)行腳本的時候需要輸入兩個參數(shù)第一個參數(shù)對應(yīng)PID,第二個參數(shù)對應(yīng)需要顯示占CPU線程堆棧的多少行數(shù)據(jù)审残。腳本文件如下:
#!/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 -- 第一個參數(shù)進(jìn)程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 -- 第二個參數(shù)顯示多少行的堆棧信息
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占用率不高,但響應(yīng)很慢
? ? ? ?在整個請求的過程中多次執(zhí)行Thread Dump然后進(jìn)行對比搅轿,取得 BLOCKED狀態(tài)的線程列表(通過我們上面提供的thread dump工具是很容易獲取到的)病涨,通常是因為線程停在了I/O、數(shù)據(jù)庫連接或網(wǎng)絡(luò)連接的地方璧坟。
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.