采用Java開發的大型應用系統越來越大,越來越復雜,很多系統集成在一起,整個系統看起來像個黑盒子。系統運行遭遇問題(系統停止響應,運行越來越慢,或者性能低下,甚至系統宕掉),如何速度命中問題的根本原因是我們接下來講的目的。本系列文章將Java問題定位的方法體系化,提供一種以黑盒子方式進行問題定位的思路:如何使用線程堆棧進行性能瓶頸分析?如何分析內存泄漏?如何分析系統掛死?
目錄
- 總述
- 如何輸出線程堆棧?
- 如何解讀線程堆棧?
- 線程的解讀
- 鎖的解讀
- 線程狀態的解讀
總述
- 系統無緣無故的cpu過高
- 系統掛起,無響應
- 系統運行越來越慢
- 性能瓶頸(如無法充分利用cpu等)
- 線程死鎖,死循環等
- 由於線程數量太多導致的內存溢出(如無法創建線程等)
借助線程堆棧可以幫助我們縮小范圍,找到突破口。線程堆棧分析很多時候不需要源代碼,在很多場合都有優勢。下面我們就開始我們的線程堆棧之旅。
如何輸出線程堆棧?
如何解讀線程堆棧?
線程的解讀


通過上節的介紹的方法打印堆棧信息,我們只關注java用戶線程,其他由虛擬機自動創建的,在實際分析中,只關心java用戶線程即可。
- <span style="font-family: SimSun;"><span style="font-size: 12px;">"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)</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() 當線程執行到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>
線程狀態的解讀
- 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。