JAVA Thread Dump 檔案分析Thread Dump介紹
Thread Dump是非常有用的診斷Java應用問題的工具。每一個Java虛擬機器都有及時生成所有執行緒在某一點狀態的thread-dump的能力,雖然各個 Java虛擬機器列印的thread dump略有不同,但是大多都提供了每個執行緒的所有資訊,例如: 執行緒狀態、執行緒 Id、本機 Id、執行緒名稱、堆疊跟蹤、優先順序。
Thread Dump特點能在各種作業系統下使用
能在各種Java應用伺服器下使用
可以在生產環境下使用而不影響系統的效能
可以將問題直接定位到應用程式的程式碼行上(對於線上排查問題非常有用)
它能幫我們解決哪些線上問題?Thread dump 能幫我們定位到 例如 CPU 峰值、應用程式中的無響應性、響應時間差、執行緒掛起、高記憶體消耗。
如何抓取Thread Dump一般當伺服器掛起,崩潰或者效能底下時,就需要抓取伺服器的執行緒堆疊(Thread Dump)用於後續的分析. 在實際執行中,往往一次 dump的資訊,還不足以確認問題。為了反映執行緒狀態的動態變化,需要接連多次做threaddump,每次間隔10-20s,建議至少產生三次 dump資訊,如果每次 dump都指向同一個問題,我們才確定問題的典型性。
獲取thread dump
JDK自帶命令列工具獲取PID,再獲取ThreadDump:1. jps 或 ps –ef|grep java (獲取PID)2. jstack [-l ]<pid> | tee -a jstack.log (獲取ThreadDump)
實操演練
獲取所有執行緒的thread dump 分兩步.
第一步 獲取程序的PID使用Jps 獲取所有java程序的資訊 第二步 選取對應的pid 例如上圖紅框中的數字串 使用Jstack獲取所有執行緒棧資訊jstack -l 9468 | tee -a jstack.log
日誌欄位分析我們把Thread dump檔案分為2個部分來理解
拿我們的例子來說:
//頭部資訊 包含 當前時間 jvm資訊2021-01-14 17:00:51Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):
//執行緒info資訊塊"ajp-nio-8019-exec-7" #75 daemon prio=5 os_prio=0 tid=0x00007fa0cc37e800 nid=0x2af3 waiting on condition [0x00007fa02eceb000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000f183aa30> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748)
執行緒info資訊塊各個引數的意義:執行緒名稱:ajp-nio-8019-exec-7
執行緒型別:daemon
優先順序: 預設是5
jvm執行緒id:tid=0x00007fa0cc37e800,jvm內部執行緒的唯一標識(透過java.lang.Thread.getId()獲取,通常用自增方式實現。)
對應系統執行緒id(NativeThread ID):nid=0x2af3,和top命令檢視的執行緒pid對應,不過一個是10進位制,一個是16進位制。(透過命令:top -H -p pid,可以檢視該程序的所有執行緒資訊)
執行緒狀態:java.lang.Thread.State: WAITING (parking)
執行緒呼叫棧資訊:用於程式碼的分析。堆疊資訊應該從下向上解讀,因為程式呼叫的順序是從下向上的。
系統執行緒狀態 (Native Thread Status)系統執行緒有如下狀態:
deadlock死鎖執行緒,一般指多個執行緒呼叫期間進入了相互資源佔用,導致一直等待無法釋放的情況。
runnable一般指該執行緒正在執行狀態中,該執行緒佔用了資源,正在處理某個操作,如透過SQL語句查詢資料庫、對某個檔案進行寫入等。
blocked執行緒正處於阻塞狀態,指當前執行緒執行過程中,所需要的資源長時間等待卻一直未能獲取到,被容器的執行緒管理器標識為阻塞狀態,可以理解為等待資源超時的執行緒。
waiting on condition執行緒正處於等待資源或等待某個條件的發生,具體的原因需要結合下面堆疊資訊進行分析。
(1)如果堆疊資訊明確是應用程式碼,則證明該執行緒正在等待資源,一般是大量讀取某種資源且該資源採用了資源鎖的情況下,執行緒進入等待狀態,等待資源的讀取,或者正在等待其他執行緒的執行等。
(2)如果發現有大量的執行緒都正處於這種狀態,並且堆疊資訊中得知正等待網路讀寫,這是因為網路阻塞導致執行緒無法執行,很有可能是一個網路瓶頸的徵兆:
網路非常繁忙,幾乎消耗了所有的頻寬,仍然有大量資料等待網路讀寫;網路可能是空閒的,但由於路由或防火牆等原因,導致包無法正常到達;所以一定要結合系統的一些效能觀察工具進行綜合分析,比如netstat統計單位時間的傳送包的數量,看是否很明顯超過了所在網路頻寬的限制;觀察CPU的利用率,看系統態的CPU時間是否明顯大於使用者態的CPU時間。這些都指向由於網路頻寬所限導致的網路瓶頸。
(3)還有一種常見的情況是該執行緒在 sleep,等待 sleep 的時間到了,將被喚醒。
waiting for monitor entry 或 in Object.wait()Moniter 是Java中用以實現執行緒之間的互斥與協作的主要手段,它可以看成是物件或者class的鎖,每個物件都有,也僅有一個 Monitor。從上圖可以看出,每個Monitor在某個時刻只能被一個執行緒擁有,該執行緒就是 "Active Thread",而其他執行緒都是 "Waiting Thread",分別在兩個佇列 "Entry Set"和"Wait Set"裡面等待。其中在 "Entry Set" 中等待的執行緒狀態是 waiting for monitor entry,在 "Wait Set" 中等待的執行緒狀態是 in Object.wait()。
(1)"Entry Set"裡面的執行緒。我們稱被 synchronized 保護起來的程式碼段為臨界區,對應的程式碼如下:
synchronized(obj) {}
當一個執行緒申請進入臨界區時,它就進入了 "Entry Set" 佇列中,這時候有兩種可能性:
該Monitor不被其他執行緒擁有,"Entry Set"裡面也沒有其他等待的執行緒。本執行緒即成為相應類或者物件的Monitor的Owner,執行臨界區裡面的程式碼;此時在Thread Dump中顯示執行緒處於 "Runnable" 狀態。該Monitor被其他執行緒擁有,本執行緒在 "Entry Set" 佇列中等待。此時在Thread Dump中顯示執行緒處於 "waiting for monity entry" 狀態。臨界區的設定是為了保證其內部的程式碼執行的原子性和完整性,但因為臨界區在任何時間只允許執行緒序列透過,這和我們使用多執行緒的初衷是相反的。如果在多執行緒程式中大量使用synchronized,或者不適當的使用它,會造成大量執行緒在臨界區的入口等待,造成系統的效能大幅下降。如果在Thread Dump中發現這個情況,應該審視原始碼並對其進行改進。(2)"Wait Set"裡面的執行緒當執行緒獲得了Monitor,進入了臨界區之後,如果發現執行緒繼續執行的條件沒有滿足,它則呼叫物件(通常是被synchronized的物件)的wait()方法,放棄Monitor,進入 "Wait Set"佇列。只有當別的執行緒在該物件上呼叫了 notify()或者notifyAll()方法,"Wait Set"佇列中的執行緒才得到機會去競爭,但是隻有一個執行緒獲得物件的Monitor,恢復到執行態。"Wait Set"中的執行緒在Thread Dump中顯示的狀態為 in Object.wait()。
通常來說,當CPU很忙的時候關注 Runnable 狀態的執行緒,反之則關注 waiting for monitor entry 狀態的執行緒。
JVM執行緒狀態NEW:每一個執行緒,在堆記憶體中都有一個對應的Thread物件。 Thread t = new Thread();當剛剛在堆記憶體中建立Thread物件,還沒有呼叫t.start()方法之前,執行緒就處在NEW狀態。在這個狀態上,執行緒與普通的java物件沒有什麼區別,就僅僅是一個堆記憶體中的物件。
RUNNABLE:該狀態表示執行緒具備所有執行條件,在執行佇列中準備作業系統的排程,或者正在執行。 這個狀態的執行緒比較正常,但如果執行緒長時間停留在在這個狀態就不正常了,這說明執行緒執行的時間很長(存在效能問題),或者是執行緒一直得不得執行的機會(存線上程飢餓的問題)。
BLOCKED:執行緒正在等待獲取java物件的監視器(也叫內建鎖),即執行緒正在等待進入由synchronized保護的方法或者程式碼塊。 synchronized用來保證原子性,任意時刻最多隻能由一個執行緒進入該臨界區域,其他執行緒只能排隊等待。
WAITING:處在該執行緒的狀態,正在等待某個事件的發生,只有特定的條件滿足,才能獲得執行機會。而產生這個特定的事件,通常都是另一個執行緒。也就是說,如果不發生特定的事件,那麼處在該狀態的執行緒一直等待,不能獲取執行的機會。 比如:A執行緒呼叫了obj物件的obj.wait()方法,如果沒有執行緒呼叫obj.notify或obj.notifyAll,那麼A執行緒就沒有辦法恢復執行;如果A執行緒呼叫了LockSupport.park(),沒有別的執行緒呼叫LockSupport.unpark(A),那麼A沒有辦法恢復執行。
TIMED_WAITING:J.U.C中很多與執行緒相關類,都提供了限時版本和不限時版本的API。TIMED_WAITING意味著執行緒呼叫了限時版本的API,正在等待時間流逝。 當等待時間過去後,執行緒一樣可以恢復執行。如果執行緒進入了WAITING狀態,一定要特定的事件發生才能恢復執行;而處在TIMED_WAITING的執行緒,如果特定的事件發生或者是時間流逝完畢,都會恢復執行。
TERMINATED:執行緒執行完畢,執行完run方法正常返回,或者丟擲了執行時異常而結束,執行緒都會停留在這個狀態。 這個時候執行緒只剩下Thread物件了,沒有什麼用了。
根據dump日誌分析下面我們根據實際的程式碼 來分析dump日誌,可以更加透明的瞭解執行緒為什麼是這個狀態
例子1:等待釋放鎖
package jstack;public class BlockedState{ private static Object object = new Object(); public static void main(String[] args) { Runnable task = new Runnable() { @Override public void run() { synchronized (object) { long begin = System.currentTimeMillis(); long end = System.currentTimeMillis(); // 讓執行緒執行5分鐘,會一直持有object的監視器 while ((end - begin) <= 5 * 60 * 1000) { } } } }; new Thread(task, "t1").start(); new Thread(task, "t2").start(); }}
先獲取object的執行緒會執行5分鐘,這5分鐘內會一直持有object的監視器,另一個執行緒無法執行處在BLOCKED狀態
"t2" prio=6 tid=0x27d7a800 nid=0x1350 waiting for monitor entry [0x2833f000] java.lang.Thread.State: BLOCKED (on object monitor) at jstack.BlockedState$1.run(BlockedState.java:17) - waiting to lock <0x1cfcdc00> (a java.lang.Object) at java.lang.Thread.run(Thread.java:662)"t1" prio=6 tid=0x27d79400 nid=0x1338 runnable [0x282ef000] java.lang.Thread.State: RUNNABLE at jstack.BlockedState$1.run(BlockedState.java:22) - locked <0x1cfcdc00> (a java.lang.Object) at java.lang.Thread.run(Thread.java:662)
透過thread dump可以看到:t2執行緒在BLOCKED (on object monitor)。waiting for monitor entry 等待進入synchronized保護的區域 ,但obj對應的 Monitor 被其他執行緒所擁有,所以 JVM執行緒的狀態是 java.lang.Thread.State: BLOCKED (on object monitor),說明執行緒等待資源。
例子2:死鎖
public class DeadLock { public static void main(String[] args) { final Object resource1 = "resource1"; final Object resource2 = "resource2"; Thread t1 = new Thread(){ public void run(){ synchronized(resource1){ System.out.println("Thread1:locked resource1"); try{ Thread.sleep(50); }catch(Exception ex){ } synchronized(resource2){ System.out.println("Thread1:locked resource2"); } } } }; Thread t2 = new Thread(){ public void run(){ synchronized(resource2){ System.out.println("Thread2:locked resource2"); try{ Thread.sleep(50); }catch(Exception ex){ } synchronized(resource1){ System.out.println("Thread2:locked resource1"); } } } }; t1.start(); t2.start(); }}
我們的這段程式碼是讓兩個執行緒互相等待物件釋放鎖,造成死鎖的情況,在這種情況下,獲取Thread dump檔案 我們會發現jvm已經提示我們死鎖了 如下:
"Thread-1" #20 prio=5 os_prio=31 tid=0x00007fb76c0e1800 nid=0x9d03 waiting for monitor entry [0x0000700004dbf000] java.lang.Thread.State: BLOCKED (on object monitor) at DeadLock$2.run(DeadLock.java:35) - waiting to lock <0x000000076af61870> (a java.lang.String) - locked <0x000000076af618b0> (a java.lang.String)"Thread-0" #19 prio=5 os_prio=31 tid=0x00007fb76c002800 nid=0x9e03 waiting for monitor entry [0x0000700004cbc000] java.lang.Thread.State: BLOCKED (on object monitor) at DeadLock$1.run(DeadLock.java:19) - waiting to lock <0x000000076af618b0> (a java.lang.String) - locked <0x000000076af61870> (a java.lang.String)
Found one Java-level deadlock:============================="Thread-1": waiting to lock monitor 0x00007fb769825b58 (object 0x000000076af61870, a java.lang.String), which is held by "Thread-0""Thread-0": waiting to lock monitor 0x00007fb769823168 (object 0x000000076af618b0, a java.lang.String), which is held by "Thread-1"Java stack information for the threads listed above:==================================================="Thread-1": at DeadLock$2.run(DeadLock.java:35) - waiting to lock <0x000000076af61870> (a java.lang.String) - locked <0x000000076af618b0> (a java.lang.String)"Thread-0": at DeadLock$1.run(DeadLock.java:19) - waiting to lock <0x000000076af618b0> (a java.lang.String) - locked <0x000000076af61870> (a java.lang.String)Found 1 deadlock.
例子3呼叫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" #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
[執行緒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)。根據案例症狀分析解決方案1 CPU佔用率不高,但響應很慢在整個請求的過程中多次執行Thread Dump然後進行對比,取得 BLOCKED狀態的執行緒列表,通常是因為執行緒停在了I/O、資料庫連線或網路連線的地方。
2 CPU飆高,load高,響應很慢一個請求過程中多次dump;對比多次dump檔案的runnable執行緒,如果執行的方法有比較大變化,說明比較正常。如果在執行同一個方法,就有一些問題了;先找到佔用CPU的程序,然後再定位到對應的執行緒,最後分析出對應的堆疊資訊。在同一時間多次使用上述的方法,然後進行對比分析,從程式碼中找到問題所在的原因.
3 請求無法響應多次dump,檢查是否有 Found one Java-level deadlock提示,死鎖經常表現為程式的停頓,或者不再響應使用者的請求。從作業系統上觀察,對應程序的CPU佔用率為零,很快會從top或prstat的輸出中消失。