1. 程式人生 > >深入理解JVM之虛擬機器效能監控與故障處理工具

深入理解JVM之虛擬機器效能監控與故障處理工具

          學習JVM的第四章,使我瞭解了JDK釋出的6個命令列工具及兩個視覺化的故障處理工具,靈活使用這些工具可以給問題處理帶來很大的便利。除了JDK自帶的工具之外,常用的故障處理工具還有很多,如果使用的而是非Sun系列的JDK,非HotSpot的虛擬機器,就需要使用對應的工具進行分析。

一:概述

        前面兩章對於虛擬機器記憶體分配與回收技術各方面的介紹,相信可以建立一套比較完整的理論基礎。這一章是從實踐的角度去了解虛擬機器記憶體管理的世界。給一個系統定位問題的時候,知識、經驗是關鍵基礎,資料是依據,工具是運用知識處理資料的手段。這裡說的資料包括:執行日誌、異常堆疊、GC日誌、執行緒快照(threaddump/javacore檔案)、堆轉儲快照(headdump/hprof檔案)等。經常使用適當的虛擬機器監控和分析工具可以加快我們分析資料、定位解決問題的速度,但在學習工具前,也應當意識到工具永遠都是知識技能的一層包裝。

二:JDK的命令列工具

  


        上圖4-1的.exe工具(這些工具都很小,是因為這些命令列工具大多數是JDK/lib/tools.jar類庫的一層薄包裝而已),它們的主要的功能程式碼是在tools類庫中實現的。

       下表是Sun JDK監控和故障處理工具

名稱

主要作用

jps

JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機器程序

jstat

JVM Statistics Monitoring Tool,用於收集HotSpot虛擬機器各方面的執行資料

jinfo

Configuration Info for Java,顯示虛擬機器配置資訊

jmap

Memory Map for Java,生成虛擬機器的記憶體轉儲快照(Heapdump檔案)

jhat

JVM Heap Dump Browser,用於分析heapdump檔案,它會建立一個HTTP/HTML伺服器,讓使用者可以在瀏覽器上檢視分析結果

jstack

Stack Trace for Java,顯示虛擬機器的執行緒快照

        還有我們注意到Sun開發團隊選擇採用Java程式碼來實現這些監控工具是有特別用意的:當應用程式部署到生產環境後,無論是直接接觸物理伺服器還是遠端Telnet到伺服器上都可能會受到限制。藉助tools.jar類庫裡面的介面,我們可以直接在應用程式中實現功能強大的監控分析功能。

jps:虛擬機器程序狀況工具

        功能:可以列出正在執行的虛擬機器程序,並顯示虛擬機器執行主類(Main Class,main()函式所在的類)名稱以及這些程序的本地虛擬機器唯一ID(Local Virtual Machine Identifier,LVMID)。(雖然功能比較單一,但它是使用頻率最高的JDK命令列工具,因為其他的JDK命令列工具大多需要輸入它查詢到LVMID來確定要監控的是哪一個虛擬機器程序。對於本地虛擬機器程序來說,LVMID與作業系統的程序ID(Process Identifier,PID)是一致的,使用Windows的工作管理員或者unix的ps命令也可以查詢到虛擬機器程序的LVMID,但如果同時啟動了多個虛擬機器程序,無法根據程序名稱定位時,那就只能依賴jps命令來顯示主類的功能才能區分了)。

        jps命令格式:

        jps [ option ] [ hostid ]


       jps可以通過RMI協議查詢開啟了RMI服務的遠端虛擬機器程序狀態,hostid為RMI登錄檔中註冊的主機名。jps的其他常用選項見下表:

  選項 

作用(jps工具主要選項)

-q

只輸出LVMID,省略主類的名稱

-m

輸出虛擬機器程序啟動時傳遞給主類main()函式的引數     

-l

輸出主類的全名,如果程序執行的是Jar包,輸出Jar路徑

-v

輸出虛擬機器程序啟動時JVM引數

jstat:虛擬機器統計資訊監視工具

         jstat(JVM Statistics Monitoring Tool)是用於監視虛擬機器各種執行狀態資訊的命令列工具。它可以顯示本地或者遠端虛擬機器程序中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料,在沒有GUI圖形介面,只提供了純文字控制檯環境的伺服器上,它將是執行期定位虛擬機器效能問題的首先工具。

         jstat命令格式為:

         jstat [ option vmid [ interval [ s | ms ] [ count ] ]  ]

         對於命令格式中的VMID與LVMID需要特別說明一下:如果是本地虛擬機器程序,VMID與LVMID是一致的,如果是遠端虛擬機器程序,那VMID的格式應當是:

         [ protocol :] [ // ] lvmid [ @hostname [ :port] /servername] 

         引數interval和count代表查詢間隔和次數,如果省略這兩個引數,說明只查詢一次。假設需要每250毫秒查詢一次程序2764垃圾收集狀況,一共查詢20次,那命令應當是:

         jstat -gc 2764 250 20

         選項option代表著使用者希望查詢的虛擬機器資訊,主要分為3類:類裝載、垃圾收集、執行期編譯狀況,具體選項及作用請參考表中的描述:

選項

作用(jstat工具主要選項)

-class

監視類裝載、解除安裝數量、總空間以及類裝載所耗費的時間

-gc

  監視Java堆狀況,包括Eden區、兩個survivor區、老年代、永久代等的容量、已用空間、GC時間合計等資訊  

-gccapacity

監視內容與-gc基本相同,但輸出主要關注Java堆各個區域使用到的最大。最小空間

-gcutil

監視內容與-gc基本相同,但輸出主要關注已使用空間佔總空間的百分比

-gccause

與-gcutil功能一樣,但是會額外輸出導致上一次GC產生的原因

-gcnew

監視新生代GC狀況

-gcnewcapacity

監視內容與-gcnew基本相同,輸出主要關注使用到的最大、最小空間

-gcold

監視老年代GC狀況

-gcoldcapacity

監視內容與-gcold基本相同,輸出主要關注使用到的最大、最小空間

-gcpermcapacity

輸出永久代使用到的最大、最小空間

-compiler

輸出JIT編譯器編譯過的方法、耗時等資訊

-printcompilation

輸出已經被JIT編譯的方法

        查詢結果表明:新生代Eden區(E,表示Eden)使用了6.2%的空間,兩個Survivor區(S0、S1,表示Survivor0、Survivor1)裡面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)則分別使用了41.42%和47.20%的空間。程式執行以來共發生Minor GC(YGC,表示Young GC)16次,總耗時0.105秒,發生Full GC(FGC,表示Full GC)3次,Full GC總耗時(FGCT,表示Full GC Time)為0.472秒,所有GC總耗時(GCT,表示GC Time)為0.577秒。

jinfo:Java配置資訊工具

        jinfo(Configuration Info for Java)的作用是實時地檢視和調整虛擬機器各項引數。

        jinfo命令格式:

        jinfo [ option ] pid

選項

作用(jinfo工具主要選項)

-v

可以檢視虛擬機器啟動時顯示指定的引數列表

-flag

檢視未被顯示指定的引數的系統預設值

 Java -XX:PrintFlagsFinal 

檢視引數預設值也是一個很好的選擇

-sysprops

  把虛擬機器程序的System.getProperties()的內容打印出來   


jmap:Java記憶體映像工具

        jmap(Memory Map for Java)命令用於生成堆轉儲快照(一般稱為headdump或dump檔案)。如果不使用jmap命令,要想獲取Java堆轉儲快照,還有一些比較“暴力”的手段:譬如在第2章中用過的-XX:+HeapOnOutOfMemoryError引數,可以讓虛擬機器在OOM異常出現之後自動生成dump檔案,通過-XX:+HeapDumpOnCtrlBreak引數則可以使用[ Ctrl ] + [ Break ]鍵讓虛擬機器生成dump檔案,又或者在Linux系統下通過Kill -3命令傳送程序退出訊號“嚇唬”一下虛擬機器,也能拿到dump檔案。

       jmap的作用並不僅僅是為了獲取dump檔案,它可以查詢finalize執行佇列、Java堆和永久代的資訊資訊,如空間使用率、當前用的是哪種收集器等。

       和jinfo命令一樣,jmap有不少功能在Windows平臺下都是受限的,除了生成dump檔案的-dump選項和用於檢視每個類的例項、空間佔用統計的-histo選項在所有作業系統都提供之外,其餘選項都只能在Linux/Solaris下使用。

       jmap命令格式:

       jmap [ option ] vmid

       option選項的合法值與具體含義見下表:

選項

作用

-dump

 生成Java堆轉儲快照。格式為:-dump:[ live, ] format=b, file=<filename>,其中live子引數說明是否只dump出存活的物件 

 -finalizerinfo 

顯示在F-Queue中等待Finalizer執行緒執行finalize方法的物件。只在Linux/Solaris平臺下有效

-heap

顯示Java堆詳細資訊,如使用哪種回收器、引數配置、分代狀況等。只在Linux/Solaris平臺下有效

-histo

顯示堆中物件統計資訊,包括類、例項數量、合計容量

-permstat

以ClassLoader為統計口徑顯示永久代記憶體狀態。只在Linux/Solaris平臺下有效

-F

當虛擬機器程序對-dump選項沒有響應時,可使用這個選項強制生成dump快照。只在Linux/Solaris平臺下有效

jhat:虛擬機器堆轉儲快照分析工具

         Sun JDK提供jhat(JVM heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。jhat內建了一個微型的HTTP/HTML伺服器,生成dump檔案的分析結果後,可以在瀏覽器中檢視。(一般不會直接使用jhat命令來分析dump檔案,原因有兩點:1,一般不會在部署應用程式的伺服器上直接分析dump檔案,即使這樣做,一般也儘量把dump檔案複製到其他機器上去分析,因為分析是一個耗時且消耗硬體資源的一個過程,既然都要在其他機器上運行了,就沒有必要受到命令列工具的限制了;2,jhat的分析功能相對來說比較醜陋,有其工具能實現比jhat更強大更專業的分析功能)。

  

        分析結果預設是以包為單位進行分組顯示,分析記憶體洩漏問題主要會使用到其中的“Heap Histogram”(與jmap -histo功能一樣)與OQL頁籤的功能,前者可以找到記憶體中總容量最大的物件,後者是標準的物件查詢語言,使用類似SQL的語法對記憶體中的物件進行查詢統計。

jstack:Java堆疊跟蹤工具

         jstack(Stack Trace for Java)命令用於生成虛擬機器當前時刻的執行緒快照(一般稱為threaddump或者javacore檔案)。執行緒快照就是當前虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死迴圈、請求外部資源導致的長時間等待都是導致執行緒長時間停頓的常見原因。執行緒出現停頓的時候使用jstack來檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒在後臺做些什麼事情,或者等待著什麼資源。

        jstack命令格式:

        jstack [ option ] vmid

 選項 

作用(jstack工具主要選項)

-F

  當正常輸出的請求不被響應時,強制輸出執行緒堆疊 

-l

除堆疊外,顯示關於鎖的附加資訊

-m

如果呼叫到本地方法的話,可以顯示C/C++的堆疊


       在JDK1.5中,java.lang.Thread類新增了一個getAllStackTraces()方法用於獲取虛擬機器中所有執行緒的StackTraceElement物件。使用這個方法可以通過簡單的幾行程式碼就完成了jstack的大部分功能,在實際專案中不妨呼叫這個方法做個管理員介面,可以隨時使用瀏覽器來檢視執行緒堆疊,如下面程式碼清單所示:

<%@ page import = "java.util.Map" %>
<html>
<head>
<title>伺服器執行緒資訊</title>
</head>
<body>
	<pre>
		<%
			for(Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()){
				Thread thread = (Thread)stackTrace.getKey();
				StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
				if(thread.equals(Thread.currentThread())){
					continue;
				}
				out.print("\n執行緒:" + thread.getName() + "\n");
				for(StackTraceElement element : stack){
					out.print("\t" + element + "\n");
				}
			}
		%>
	</pre>
</body>
</html>

HSDIS:JIT生成程式碼反彙編

        HSDIS是一個Sun官方推薦的HotSpot虛擬機器JIT編譯程式碼的反彙編外掛,它包含在HotSpot虛擬機器原始碼之中,但沒有提供編譯後的程式。在Project Kenai的網站也可以下載到單獨的原始碼。它的作用是讓HotSpot的-XX:+PrintAssembly指令呼叫它來把動態生成的原生代碼還原為彙編程式碼輸出,同時還生成了大量非常有價值的註釋,這樣我們就可以通過輸出的程式碼來分析問題。讀者可以根據自己的作業系統和CPU型別從Project Kenai的網站上下載編譯好的外掛,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目錄中即可。如果沒有找到所需作業系統(譬如windows的就沒有)的成品,那就得自己使用原始碼編譯一下。(值得注意的是:如果讀者使用的是Debug或者FastDebug版的HotSpot,那就可以直接通過-XX:PrintAssembly指令使用外掛;如果使用的是Product版的HotSpot,那還需要額外加入一個-XX:UnlockDiagnosticVMOptions引數)。JVM書的作者曾以下面簡單地程式碼為例演示了這個外掛的使用。

public class Bar{
	
	int a = 1;
	static int b = 2;
	
	public int sum(int c){
		return a + b + c;
	}
	
	public static void main(String[] args) {
		new Bar().sum(3);
	}
}

       其中,引數-Xcomp是讓虛擬機器以編譯模式執行程式碼,這樣程式碼就可以偷懶,不需要執行足夠次數來預測就能觸發JIT編譯。兩個-XX:CompileCommand意思是讓編譯期不需要內聯sum()並且只編譯sum(),-XX:+PrintAssembly就是輸出反彙編內容。如果一切順的話,就可以出現如下圖所示的內容:

        對上圖程式碼進行解釋:

       1:mov %eax,-0x8000(%esp):檢查棧溢;

       2:push %ebp:儲存上一幀棧基址;

       3:sub $0x18,%esp:給新幀分配空間;

       4:mov 0x8(%ecx) ,%eax:取例項變數a,這裡0x8(%ecx)的意思是ecx+0x8的意思,前面“[Constants]”節中提示了“this.ecx = ‘test/Bar’”,即ecx存放的是this物件的地址。偏移0x8是越過this物件的物件頭,之後就是例項變數a的記憶體位置。這次是訪問“Java堆”中的資料;

       5:mov $0x3d2fad8,%esi:取test.Bar方法區的指標;

       6:mov 0x68(%esi),%esi:取類變數b,這次是訪問“方法區”中的資料;

       7:add %esi,%eax和add %edx,%eax:做兩次加法,求a+b+c的值,前面的程式碼把a放在eax中,把b放在esi中,而c在[Constants]中提示了,“parm0:edx = int”,說明c在edx中;

      8:add $0x18,%esp:撤銷幀棧;

      9:pop %ebp:恢復上一幀棧;

     10:test %eax,0x2b0100:輪詢方法返回處的SafePoint;

     11:ret:方法返回

三:JDK視覺化工具

       JDK中除了提供大量的命令列工具外,還有兩個功能強大的視覺化工具:JConsole和VisualVM,這兩個工具是JDK的正式成員,沒有被貼上“unsupported and experimental”的標籤。

JConsole:Java監視與管理控制檯

        JConsole(Java Monitoring and Management Console)是一種基於JMX的視覺化監視、管理工具。它管理部分的功能是針對JMX MBean進行管理,由於MBean可以使用程式碼、中間伺服器的管理控制檯或者所有符合JMX規範的軟體進行訪問。

       1:啟動JConsole:

        通過JDK/bin目錄下的“jconsole.exe”啟動JConsole後,將自動搜尋出本機執行的所有虛擬機器程序,不需要使用者再使用jps來查詢了。如下圖所示:雙擊選擇其中一個程序即可進行監控,也可以使用下面的“遠端程序”功能來連線遠端伺服器,對遠端虛擬機器進行監控。


        選擇monitoring這個雙擊它進入JConsole主頁面


       2:記憶體監控

       “記憶體”頁籤相當於視覺化的jstat命令,用於監視受收集管理的虛擬機器記憶體(Java堆和永久代)的變化趨勢。

import java.util.ArrayList;
import java.util.List;

public class JConsoleMonitor {
	
	static class OOMObject{
		public byte[] placeholder = new byte[64 * 1024];
	}
	
	public static void fillHeap(int num)throws InterruptedException{
		List<OOMObject> list = new ArrayList<OOMObject>();
		for(int i = 0; i < num; i++){
			//稍作延遲,令監視曲線的變化更加明顯
			Thread.sleep(50);
			list.add(new OOMObject());
		}
		System.gc();
	}
	
	public static void main(String[] args)throws Exception {
		fillHeap(1000);
	}
}
       執行上面程式程式碼可以在“記憶體”頁籤中可以看到記憶體池Eden區的執行趨勢呈折線,如下圖所示:

       但是監視範圍擴大至整個堆後,會發現曲線是一條向上增長的平滑曲線。並且從柱狀圖可以看出,在1000次迴圈執行結束,運行了System.gc()後;雖然整個新生代Eden和Survivor區基本都被清空了,但是代表老年代的柱狀圖仍然保持巔峰值狀態,說明被填充進堆中的資料在System.gc()方法執行之後仍然存活。但是有兩個問題:

       1:虛擬機器啟動引數只限制了Java堆為100MB,沒有指定-Xmn引數,能否從監控圖中估計出新生代有多大?

        上圖顯示Eden空間為27328KB,因為沒有設定-XX:SurvivorRadio引數,所以Eden與Survivor空間比例預設值為8:1,整個新生代空間大約為27328KB*125%=34160KB;

       2:為何執行了System.gc()之後,上面的圖老年代依然顯示巔峰值狀態,程式碼需要做何調整才能讓System.gc()回收掉填充到堆中的物件?

       執行完System.gc()之後,空間未能回收是因為List<OOMObject> list物件仍然存活,fillHeap()方法仍然沒有退出,因此list物件在System.gc()執行時仍然處於作用域之內,如果把System.gc()移動到fillHeap()方法之外呼叫就可以回收掉全部記憶體。

       3:執行緒監控
       如果上面的“記憶體”頁籤相當於視覺化的jstat命令的話,“執行緒”頁籤的功能相當於視覺化的jstack命令,遇到執行緒停頓時可以使用這個頁籤進行監控分析。前面講解jstack命令的時候提到過執行緒長時間停頓的主要原因主要有:等待外部資源(資料庫連線、網路資源、裝置資源等)、死迴圈、鎖等待(活鎖和死鎖)。

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ThreadWait {
	/**
	 * 執行緒死迴圈演示
	 */
	public static void createBusyThread(){
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run(){
				while(true); //第41行 死迴圈
			}
		}, "testBusyThread");
		thread.start();
	}
	/**
	 * 執行緒鎖等待演示
	 */
	public static void createLockThread(final Object lock){
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run(){
				synchronized (lock) {
					try{
						lock.wait(); //執行緒鎖等待
					}catch(InterruptedException e){
						e.printStackTrace();
					}
				}
			}
		},"testLockThread");
		thread.start();
	}
	public static void main(String[] args)throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		br.readLine();
		createBusyThread();
		br.readLine();
		Object obj = new Object();
		createLockThread(obj);
	}
}
        程式執行後,首先在“執行緒”頁籤中選擇main執行緒,如下圖所示:堆疊追蹤顯示BufferedReader在readBytes方法中等待System.in的鍵盤輸入,這是執行緒為Runnable狀態,Runnable狀態的執行緒會被分配執行時間,但readBytes方法檢查到流沒有更新時會立刻歸還執行令牌,這種等待只消耗很小的CPU資源。


        接著監控testBusyThread執行緒,如下圖所示,testBusyThread執行緒一直在執行空迴圈,從堆疊追蹤中看到一直在MonitoringTest.java程式碼的41行停留,41行為:while(true)。這時候執行緒為Runnable狀態,而且沒有歸還執行緒執行令牌的動作,會在空迴圈上用盡全部執行時間直到執行緒切換,這種等待會消耗較多的CPU資源。


       下圖顯示testLockThread執行緒在等待著lock物件的notify或notifyAll方法的出現,執行緒這時處於WAITING狀態,在被喚醒前不會被分配執行時間。


        testLockThread執行緒正在處於正常的活鎖等待,只要lock物件的notify()或notifyAll()方法被呼叫,這個執行緒便能啟用以繼續執行。下面程式碼演示了一個無法再被啟用的死鎖等待。

public class DeadlockWait {
	
	/**
	 * 執行緒死鎖等待演示
	 */
	static class SynAddRunnable implements Runnable{
		int a, b;
		public SynAddRunnable(int a, int b){
			this.a = a;
			this.b = b;
		}
		@Override
		public void run(){
			synchronized (Integer.valueOf(a)) {
				synchronized (Integer.valueOf(b)) {
					System.out.println(a+b);
				}
			}
		}
	}
	
	public static void main(String[] args){
		for(int i = 0; i < 100; i++){//開啟200個執行緒分別取計算1+2以及2+1的值,其實for迴圈可以省略,兩個執行緒也可能會出現死鎖,不過概率太小
			new Thread(new SynAddRunnable(1,2)).start();
			new Thread(new SynAddRunnable(2,1)).start();
		}
	}
}

       這段程式碼原因是Integer.valueOf()方法基於減少物件建立次數和節省記憶體的考慮,[ -128, 127]之間的數字會被快取,當valueOf()方法傳入引數在這個範圍之內,將直接返回快取中的物件。也就是說,程式碼中呼叫了200次Integer.valueOf()方法一共就只返回了兩個不同的物件。假如在某個執行緒的兩個synchronized塊之間發生了一次執行緒切換,就會出現執行緒A等著執行緒B持有的Integer.valueOf(1),執行緒B又等著被執行緒A持有的Integer.valueOf(2),結果出現大家都跑不下去的情景。

        出現執行緒死鎖之後,點選JConsole執行緒面板的“檢測到死鎖”按鈕,將出現一個新的“死鎖”頁籤,如下圖所示,很清楚的看大Thread-43在等待一個被執行緒Thread-12持有的Integer物件,而點選執行緒Thread-12則顯示它也在等待一個Integer物件,被執行緒Thread-43持有,這樣兩個執行緒就相互卡主,都不存在等到鎖釋放的希望了。


VisualVM:多合一故障處理工具

        VisualVM(All-in-One Java Troubleshooting Tool)是到目前為止隨著JDK釋出的功能最強大的執行監視和故障處理程式。它的效能分析也比一些收費軟體不相上下,它還有一個優點:不需要被監視的程式基於特殊Agent執行。

       1:VisualVM兼容範圍與外掛安裝


        通過外掛擴充套件支援,VisualVM可以做到:


       2.生成、瀏覽堆轉儲快照


       3.分析程式效能

       4.BTrace動態日誌跟蹤