Java Thread Dumps分析
原文地址:http://java.sys-con.com/node/1611555
作者:Shankar Itchapurapu(yakoo5譯於2014.05.01)
一、 概述:
軟件運維是一項極其枯燥乏味而又非常具有挑戰性的工作,就像軟件功能被期望的很美好那樣。想象下半夜被手機的不停的震動吵醒一樣,感覺很不爽吧?
任何軟件系統,不管它被構建和質量測試的多麼好都可能會出現運行時的性能問題。問題可能是由系統內部產生,也可能是由於外部環境所致。軟件系統是建立在一定假設和一些先入爲主的觀念基礎上。然而,當這些系統開始真正運行時,這些假設可能失效從而導致系統故障。
對於企業級J2EE系統,它們通常擁有非常大的用戶數量基礎,而且會涉及很多異構系統間的互操作,其中一個最常見的運行時問題就是系統性能下降或者系統“掛起”。碰到這種情況,通常的故障排除做法就是分析Java線程轉儲,以便找出那些導致系統系能下降或系統掛起的線程。本文討論了Javastack traces、Java線程的解剖結構以及如何閱讀常見的線程轉儲。
二、 Exceptions and Stack Traces
我相信大家在學習和開發階段或多或少都會遇到過異常(exceptions)。Java通過Exception來告知一個運行時錯誤。Exceptions包含兩部分:異常消息(Message)和堆棧跟蹤信息(Stack traces)。異常消息可以告訴你出什麼錯了,而堆棧跟蹤信息可以提供能夠了解所有與本次運行時錯誤有關的自上而下的類方法調用流程信息。
下面是一個ArrayIndexOutOfBoundsException(數組越界)的堆棧跟蹤信息的例子:
Exception in thread "main"java.lang.ArrayIndexOutOfBoundsException: 4
at Test.run(Test.java:13)
at Test.<init>(Test.java:5)
at Test.main(Test.java:20)
在上面的異常信息中,第一行“Exception in thread"main" java.lang.ArrayIndexOutOfBoundsException: 4”說明JVM在嘗試訪問數組索引號爲4的值時拋出了異常,拋出異常的Java線程是“main”線程。
下面我們來大體過下stack trace信息。異常處理第一條:先看下第一行(包含異常消息的行)來了解是什麼異常,然後再接着往下看來了解調用流程。在上面的例子中,調用從Test.java的第20行(main()方法)開始,然後開始調用Test類的構造器。構造器(Constructors)在stack traces裏面以<init>方式顯示,然後接着執行Test類的run()方法,在執行第13行時發現並拋出了異常。
從上面的stack trace,我們可以看出Test.java嘗試讀取的內容位置超出了數組的大小邊界。
三、 Java線程轉儲(Thread Dump)
Java Thread dump可以被認爲是在某一時刻JVM中所有線程的一個快照。線程轉儲可能包含一個或多個線程。在多線程環境中,比如像J2EE應用服務器中,會有很多線程和線程組。每一個線程都有它們自己的調用棧來執行各自負責的任務。Thread Dump提供所有這些JVM線程的站跟蹤信息,以及關於某一特定線程的詳細信息。
四、 Java虛擬機進程和Java線程(Java VM Process and Java Threads)
Java虛擬機或JVM是一個操作系統級的進程,Java線程是它的子進程或者輕量級進程(Solaris的說法)。
五、 生成Java線程轉儲(Generating Java Thread Dumps)
可以通過向JVM進程發送SIGQUIT信號來生成線程轉儲,有幾種不同的方式可以發送這個信號給JVM進程:
在Unix中,可以使用“kill -3<pid>”命令,pid是JVM的進程ID;
在Windows中,可以在運行JVM所在的窗口按CTRL+BREAK組合鍵。
六、 Java線程的3種狀態(Thread State)
每一個Java線程在它們的生命週期中都可能會處於下面3個狀態中的任意一種狀態。
Runnable
線程正在運行或者它已經拿到了它的CPU時間片而準備去運行。在JRockit虛擬機的線程轉儲中,這個狀態標記爲ACTIVE。
Waiting on Monitor
線程處於sleeping或者在一定時間內在一個對象上面等待,亦或者等待被另外一個線程喚醒(notifiy)。通常在調用Thread對象的sleep()方法或者某個對象的wait()方法時將會使線程會處於這個狀態。
例如,在WebLogic服務器中,一些空閒的execute線程在等待一個socket讀取線程去通知它們處理時,就會處於Waiting on Monitor狀態。它們的stack trace跟下面這段類似:
"ExecuteThread: '2' for queue:'weblogic.admin.RMI'" daemon prio=5 tid=0x1752F040 nid=0x180c inObject.wait() [1887f000..1887fd8c]
at java.lang.Object.wait(Native Method) waiting on<04134D98> (a weblogic.kernel.ExecuteThread)
at java.lang.Object.wait(Object.java:426)
atweblogic.kernel.ExecuteThread.waitForRequest(ExecuteThread.java:126)
locked <04134D98> (a weblogic.kernel.ExecuteThread)
atweblogic.kernel.ExecuteThread.run(ExecuteThread.java:145)
一些其他版本的JVM也把這個狀態叫做CW狀態(Condition Waiting),Object.wait()(如上所述)。JRockit虛擬機把這個狀態標記爲WAITING。
Waiting forMonitor Entry
表示線程正在等待一個對象的鎖(一些其它的線程可能持有這個鎖),當兩個及以上的線程嘗試執行同步代碼時會出現這種情況。一定要注意,鎖總是針對於對象而不是某些個別的方法。
下面是一個處於Waiting for Monitor Entry狀態的線程stack trace示例:
"ExecuteThread: '24' for queue:'DisplayExecuteQueue'" daemon prio=5 tid=0x5541b0 nid=0x3b waiting formonitor entry [49b7f000..49b7fc24]
atweblogic.cluster.replication.ReplicationManager.createSecondary(ReplicationManager.java:908)
- waiting to lock <6c4b9130> (a java.lang.Object)
at weblogic.cluster.replication.ReplicationManager.updateSecondary(ReplicationManager.java:715)
at weblogic.servlet.internal.session.ReplicatedSessionData.syncSession(ReplicatedSessonData.java:459)
- locked <6c408700> (a weblogic.servlet.internal.session.ReplicatedSessionData)
at weblogic.servlet.internal.session.ReplicatedSessionContext.sync(ReplicatedSessionContext.java:134)
- locked <6c408700>(aweblogic.servlet.internal.session.ReplicatedSessionData)
at weblogic.servlet.internal.ServletRequestImpl.syncSession(ServletRequestImpl.java:2418)
at weblogic.servlet.internal.WebAppServletContext.invokeServlet(WebAppServletContext.java:3137)
at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2544)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:153)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:134)
從上面的stack trace中,你可以看到這個線程持有一個對象鎖(地址爲6c408700),同時又在等待鎖另一個對象(地址爲6c4b9130)。
一些其它的JVM可能並不會在stack trace中提供包含有鎖相關信息的對象的Id,同樣的狀態也可以被叫做“MW”狀態。JRockit虛擬機把這個狀態標記爲LOCKED。
七、 Java線程解剖(Anatomy of a Java Thread)
要想能夠讀懂或者分析Java線程轉儲,就必須要搞清楚線程轉儲的各個組成部分。下面我們通過一個thread stack樣例進行舉例,然後看看它的各個部分的內容。
"ExecuteThread: '1' " daemonprio=5 tid=0x628330 nid=0xf runnable [0xe4881000..0xe48819e0]
at com.vantive.vanjavi.VanJavi.VanCreateForm(Native Method)
at com.vantive.vanjavi.VanMain.open(VanMain.java:53)
at jsp_servlet._so.__newServiceOrder.printSOSection(__newServiceOrder.java:3547)
at jsp_servlet._so.__newServiceOrder._jspService (__newServiceOrder.java:5652)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:265)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:200)
at weblogic.servlet.internal.WebAppServletContext.invokeServlet(WebAppServletContext.java:2495)
at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2204)
at weblogic.kernel.ExecuteThread.execute (ExecuteThread.java:139)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:120)
在上面的線程轉儲中,我們最感興趣的是第一行,至於其它的部分無外乎就是一些常見的stack trace內容。下面我來看看第一行都提供了什麼信息:
Execute Thread : 1說明線程名稱是"ExecuteThread: '1' ";
daemon表示這個線程是一個daemon線程;
prio=5線程的優先級 (默認是5);
tid Java 線程Id (在運行的JVM實例中的線程唯一標識符);
nid線程的本地Id(Native Identifier),在Solaris系統中指的是LWP id, 操作系統級的進程Id;
runnable線程狀態(如上所述);
[x..y]指這個線程在java heap裏面執行的地址範圍;
剩下的線程轉儲內容說明了調用流程,在這個例子中,線程(ExecuteThread 1)是一個正在執行本地vanCreateForm()方法的操作系統的後臺daemon線程。
八、 如何使用線程轉儲(Putting Thread Dumps to Use)
在這個部分,我會給大家介紹一些java線程轉儲非常有用的案例。
(一) 高CPU佔用率(High CPU consumption)
診斷分析(Diagnosis)
應用程序似乎消耗了幾乎100%CPU佔用率,同時系統吞吐量有顯著下降,在高負載CPU的情況下,系統性能開始變的很低。
線程轉儲(Thread Dump)
線程轉儲中所有線程大都表現出一個或多個線程在所有的線程轉儲時都在執行相同的操作。
解決方案(Solution)
-
選擇一個特定的調用流程(例如:一個web的表單提交調用流程),在調用完成前採取5~7次線程轉儲操作;
-
在線程轉儲中查找處於“runnable”狀態的線程。如果每個這樣的線程看起來都在繼續執行(每一個線程轉儲中的方法調用都不同),說明這個線程一直在處理中,應該不是罪魁禍首。如果在整個線程轉儲中,線程都在執行相同的方法(相同的行號),那麼幾乎可以非常確定這個線程就是罪魁禍首。然後找到這段代碼,做一個代碼級別的分析,你幾乎就可以確定找到了問題所在。
(二) CPU佔用率很低,但是響應時間卻很糟糕(Low CPU consumption and Poor Response time)
診斷分析(Diagnosis)
通常在I/O非常密集型的系統中,每當系統負載過高時,都會發生這樣的情況。CPU佔用率低是因爲只有少數的線程在使用合理的CPU時間片。
線程轉儲(Thread Dump)
一些或者所有處於runnable狀態的線程似乎都在執行I/O相關的操作,比如:一個文件讀/寫或者數據庫相關的操作。
解決方案(Solution)
簡要了解應用程序的I/O操作後,我們可以使用緩存(如果可以的話)來減少與數據庫的交互。
(三) 應用程序/服務器掛起
診斷分析(Diagnosis)
某個用程序或者JVM託管應用程序的服務器將會掛起(變得無法響應服務請求)。
線程轉儲(Thread Dump)
-
在所有線程轉儲期間,發現所有處於runnable狀態的線程都在執行相同的操作。服務器無法創建更多的線程,因爲所有的runnable線程“永遠”都無法完成它們的任務;
-
這可能是由於許多線程都在等待一個monitor entry。如果一個“runnable”的線程持有某個對象上的鎖,並且從不返回這個鎖,而同時其它線程又都在等待同一個鎖時,就會發生這種情況。
解決方案(Solution)
-
檢查死鎖。JVM通常是可以檢測到一些簡單的死鎖(比如:線程A正在等待線程B,反之亦然)。然而,你需要了解在某個特定時刻鎖的相關情況,以確定是否有涉及任何複雜的死鎖場景;
-
重審代碼中的同步方法或同步塊,儘可能減少同步區域的大小;
-
其中一個問題可能是在訪問一個遠程資源或組件時超時時間過長。可以實現一個具有合理超時限制的遠程對象客戶端,以便在遠程系統未在規定時間內響應時拋出一個合適的異常;
-
如果所有的線程都在等待一個資源(比如:EJB/DB連接),可以考慮增加這些資源池的大小。
九、 工具(Tools)
線程轉儲分析,在商業領域和開源領域都有一些可用的工具。Samurai(中文譯爲:日本武士)就是其中之一,Samurai是一個輕量級的開源工具,它既可以通過命令提示行啓動,也可以以JavaWeb Start應用程序方式運行。關於Samurai的更多信息和文檔,可以訪問:http://yusuke.homeip.net/samurai/en/index.html
十、 結論(Conclusion)
在生產環境中維護企業級J2EE應用程序是一項非常艱鉅的任務。隨着業務的不斷變化,J2EE應用環境的變化,都可能會導致生產環境的應用程序運行不穩定,其中一個主要因素就是高負載。雖然,大部分系統都被設計爲可擴展的,但是環境的限制仍然可能會導致這些系統變得無法響應。
Java線程轉儲是一個用來識別、診斷、檢測和解決典型的生產系統問題的很好機制。雖然應用程序分析和其它的一些機制確實存在,分析Java線程轉儲可以讓我們對一些常見的生產系統級問題有一個清晰、早期的認識,並且爲我們節省了時間,幫助我們爲生產應用程序提供更好的用戶體驗。