Java問題定位之Java線程堆棧分析


采用Java開發的大型應用系統越來越大,越來越復雜,很多系統集成在一起,整個系統看起來像個黑盒子。系統運行遭遇問題(系統停止響應,運行越來越慢,或者性能低下,甚至系統宕掉),如何速度命中問題的根本原因是我們接下來講的目的。本系列文章將Java問題定位的方法體系化,提供一種以黑盒子方式進行問題定位的思路:如何使用線程堆棧進行性能瓶頸分析?如何分析內存泄漏?如何分析系統掛死?

目錄

 

  • 總述
  • 如何輸出線程堆棧?
  • 如何解讀線程堆棧?
    • 線程的解讀
    • 鎖的解讀
    • 線程狀態的解讀

總述

什么是線程堆棧?線程堆棧也稱線程調用堆棧,是虛擬機中線程(包括鎖)狀態的一個瞬間快照,即系統在某一個時刻所有線程的運行狀態,包括每一個線程的調用堆棧,鎖的持有情況。雖然不同的虛擬機打印出來的格式有些不同,但是線程堆棧的信息都包含:
1、線程名字,id,線程的數量等。
2、線程的運行狀態,鎖的狀態(鎖被哪個線程持有,哪個線程在等待鎖等)
3、調用堆棧(即函數的調用層次關系)調用堆棧包含完整的類名,所執行的方法,源代碼的行數。
借助堆棧信息可以幫助分析很多問題,如線程死鎖,鎖爭用,死循環,識別耗時操作等等。在多線程場合下的穩定性問題分析和性能問題分析,線程堆棧分析濕最有效的方法,在多數情況下,無需對系統了解就可以進行相應的分析。
由於線程堆棧是系統某個時刻的線程運行狀況(即瞬間快照),對於歷史痕跡無法追蹤。只能結合日志分析。總的來說線程堆棧是多線程類應用程序非功能型問題定位的最有效手段,最善於分析如下類型問題:
  • 系統無緣無故的cpu過高
  • 系統掛起,無響應
  • 系統運行越來越慢
  • 性能瓶頸(如無法充分利用cpu等)
  • 線程死鎖,死循環等
  • 由於線程數量太多導致的內存溢出(如無法創建線程等)

借助線程堆棧可以幫助我們縮小范圍,找到突破口。線程堆棧分析很多時候不需要源代碼,在很多場合都有優勢。下面我們就開始我們的線程堆棧之旅。

如何輸出線程堆棧?

Java虛擬機提供了線程轉儲(thread dump)的后門,通過這個后門可以把線程堆棧打印出來。通常我們將堆棧信息重定向到一個文件中,便於我們分析,由於信息量太大,很可能超出控制台緩沖區的最大行數限制造成信息丟失。這里介紹一個jdk自帶的打印線程堆棧的工具,jstack用於打印出給定的Java進程ID或core file或遠程調試服務的Java堆棧信息。
示例:$jstack –l 23561 >> xxx.dump 
命令 : $jstack [option] pid >> 文件  
>>表示輸出到文件尾部,實際運行中,往往一次dump的信息,還不足以確認問題,建議產生三次dump信息,如果每次dump都指向同一個問題,我們才確定問題的典型性。

 

如何解讀線程堆棧?

線程的解讀

如下面一段java源代碼程序:
 

 

通過上節的介紹的方法打印堆棧信息,我們只關注java用戶線程,其他由虛擬機自動創建的,在實際分析中,只關心java用戶線程即可。

 

 

  1. <span style="font-family: SimSun;"><span style="font-size: 12px;">"main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]  
  2.     at java.lang.String.indexOf(String.java:1352)  
  3.     at java.io.PrintStream.write(PrintStream.java:460)  
  4.     - locked <0xc8bf87d8> (a java.io.PrintStream)  
  5.     at java.io.PrintStream.print(PrintStream.java:602)  
  6.     at MyTest.fun2(MyTest.java:16)  
  7.     - locked <0xc8c1a098> (a java.lang.Object)  
  8.     at MyTest.fun1(MyTest.java:8)  
  9.     - locked <0xc8c1a090> (a java.lang.Object)  
  10.     at MyTest.main(MyTest.java:26)</span></span>  
"main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8] at java.lang.String.indexOf(String.java:1352) at java.io.PrintStream.write(PrintStream.java:460) - locked <0xc8bf87d8> (a java.io.PrintStream) at java.io.PrintStream.print(PrintStream.java:602) at MyTest.fun2(MyTest.java:16) - locked <0xc8c1a098> (a java.lang.Object) at MyTest.fun1(MyTest.java:8) - locked <0xc8c1a090> (a java.lang.Object) at MyTest.main(MyTest.java:26)

從上面的main線程看,線程堆棧里面的最直觀的信息是當前線程的調用上下文,即從哪個函數調用到哪個函數(從下往上看),正執行到哪一類的哪一行,借助這些信息,我們就對當前系統正在做什么一目了然。

另外,從main線程的堆棧中,有-locked<0xc8c1a090>(a java.lang.Object) 語句,這表示該線程已經占用了鎖,其中0xc8c1a090表示鎖ID,這個鎖ID是系統自動生成的,我們只需要知道每次打印堆棧,同一個ID表示是同一個鎖即可

 

其中"線程對應的本地線程Id號"所指的本地線程是指該java虛擬機所對應的虛擬機中的本地線程,我們知道java是解析型語言,執行的實體是java虛擬機,因此java代碼是依附於java虛擬機的本地線程執行的,之前文章中講過,當啟動一個線程時,是創建一個native本地線程,本地線程才是真實的線程實體,為了更加深入理解本地線程和java線程的關系,我們可以通過以下方式將java虛擬機的本地線程打印出來:

1、試用ps -ef|grep java  獲得java進行id

2、試用pstack<java pid> 獲得java虛擬機本地線程的堆棧

從操作系統打印出來的虛擬機的本地線程看,本地線程數量和java線程數量是相同的,說明二者是一一對應的關系。

我們獲取的本地線程堆棧如下:

 

這個本地線程號如何與java線程堆棧文件對應起來呢,每一個線程都有tid,nid的屬性,通過這些屬性可以對應相應的本地線程,我們先看java線程第一行,里面有一個屬性是nid,

 

main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8] 

其中nid是native thread id,也就是本地線程中的LWPID,二者是相同的,只不過java線程中的nid用16進制表示,本地線程的id用十進制表示。3368的十六進制表示0xd28,在java線程堆棧中查找nid為0xd28就是本地線程對應的java線程。

 

 

 

鎖的解讀

在介紹線程堆棧的解讀方法之前,先介紹一點關於多線程的知識,即wait和sleep的重要區別。wait和sleep有一個共同點,就是二者都把當前線程阻塞住,我們叫睡眠或等待,二者有着本質區別:
  • wait()  當線程執行到wait()方法上,當前線程會釋放監視鎖,此時其他線程可以占有該鎖,一旦wait()方法執行完成,當前線程繼續持有該鎖,直到執行完鎖的作用域。結合notify(),可以實現兩個線程之間的通信,一個線程可以通過這種方法通知另一個線程繼續執行,完成線程之間的配合。wait()鎖的示意圖

在wait(5000)這個期間,當前線程會釋放它占用的鎖,其他線程有機會獲得到該鎖,當wait(5000)結束后,當前線程繼續獲取該鎖的使用權。滿足以下條件之一,wait退出:

1、達到等待時間之后,自動退出

2、其他線程調用了該鎖的notify方法,如果多個線程在等待同一個鎖,只有一個線程會被通知到。

 

  • sleep() 和鎖操作無關,如果該方法恰好在一個鎖的保護范圍內,當前線程即使執行sleep的時候,仍然保持監視鎖。

 

sleep方法是線程的一個靜態方法,實際上和鎖操作無關,不會產生特別的鎖,如果原來持有,現在仍然持有,如果原來沒有,現在仍然沒有。

從上面介紹的線程堆棧看,線程堆棧中包含直接信息為:線程個數,每個線程調用的方法堆棧,當前鎖的狀態。從線程個數可以直接數出來,線程調用的方法堆棧,從下向上看,表示了當前線程調用哪個類哪個方法,鎖的狀態看起來需要一些技巧,與鎖相關的重要信息如下:

 

  • 當一個線程占有一個鎖的時候,線程堆棧會打印一個-locked<0x22bffb60>
  • 當一個線程正在等在其他線程釋放該鎖,線程堆棧會打印一個-waiting to lock<0x22bffb60>
  • 當一個線程占有一個鎖,但又執行在該鎖的wait上,線程堆棧中首先打印locked,然后打印-waiting on <0x22c03c60>
在線程堆棧中與鎖相關的三個最重要的特征字:locked,waiting to lock,waiting on 了解這三個特征字,就可以對鎖進行分析了。
一般情況下,當一個或一些線程正在等待一個鎖的時候,應該有一個線程占用了這個鎖,即如果有一個線程正在等待一個鎖,該鎖必然被另一個線程占用,從線程堆棧中看,如果看到waiting to lock<0x22bffb60>,應該也應該有locked<0x22bffb60>,大多數情況下確實如此,但是有些情況下,會發現線程堆棧中可能根本沒有locked<0x22bffb60>,而只有waiting to ,這是什么原因呢,實際上,在一個線程釋放鎖和另一個線程被喚醒之間有一個時間窗,如果這個期間,恰好打印堆棧信息,那么只會找到waiting to ,但是找不到locked 該鎖的線程,當然不同的JAVA虛擬機有不同的實現策略,不一定會立刻響應請求,也許會等待正在執行的線程執行完成。
 

線程狀態的解讀

借助線程堆棧信息,可以分析很多問題,其中cpu的消耗分析也是線程堆棧分析的一個重要內容。
java線程狀態有以下幾類:
 
  • RUNNABLE 從虛擬機的角度看,線程正在運行狀態。
處於RUNNABLE狀態的線程是不是一定會消耗cpu呢,不一定,像socket IO操作,線程正在從網絡上讀取數據,盡管線程狀態RUNNABLE,但實際上網絡io,線程絕大多數時間是被掛起的,只有當數據到達后,線程才會被喚起,掛起發生在本地代碼(native)中,虛擬機根本不一致,不像顯式的調用sleep和wait方法,虛擬機才能知道線程的真正狀態,但在本地代碼中的掛起,虛擬機無法知道真正的線程狀態,因此一概顯示為RUNNABLE。
 
  • TIMED_WAITING(on object monitor)表示當前線程被掛起一段時間,說明該線程正在執行obj.wait(ing time)方法,該線程不消耗cpu。
  • TIMED_WAITING(sleeping) 表示當前線程被掛起一段時間,正在執行Thread.sleep(int time )方法,如:
  • WAITING(on object monitor)當前線程被掛起,正在執行無參數的obj.wait()方法,只能通過notify喚醒,因此不消耗cpu
 
總結:
  • 處於timed_waiting,waiting狀態的線程一定不消耗cpu,處於runnable狀態的線程不一定會消耗cpu,要結合當前線程代碼的性質判斷,是否消耗cpu
  • 如果是純java運算代碼,則消耗cpu
  • 如果網絡io,很少消耗cpu
  • 如果是本地代碼,集合本地代碼的性質,可以通過pstack獲取本地的線程堆棧,如果是純運算代碼,則消耗cpu,如果被掛起,則不消耗,如果是io,則不怎么消耗cpu。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM