JDK視覺化故障處理工具

一直不懂發表於2020-12-25

JDK中除了附帶大量的命令列工具外,還提供了幾個功能整合度更高的視覺化工具,使用者可以使 用這些視覺化工具以更加便捷的方式進行程式故障診斷和除錯工作。這類工具主要包括JConsole、 JHSDB、VisualVM和JMC四個。其中,JConsole是最古老,早在JDK 5時期就已經存在的虛擬機器監控 工具,而JHSDB雖然名義上是JDK 9中才正式提供,但之前已經以sa-jdi.jar包裡面的HSDB(視覺化工具)和CLHSDB(命令列工具)的形式存在了很長一段時間[1]。它們兩個都是JDK的正式成員,隨著 JDK一同釋出,無須獨立下載,使用也是完全免費的。

VisualVM在JDK 6 Update 7中首次釋出,直到JRockit Mission Control與OracleJDK的融合工作完成之前,它都曾是Oracle主力推動的多合一故障處理工具,現在它已經從OracleJDK中分離出來,成為一 個獨立發展的開源專案[2]。VisualVM已不是JDK中的正式成員,但仍是可以免費下載、使用的。

Java Mission Control,曾經是大名鼎鼎的來自BEA公司的圖形化診斷工具,隨著BEA公司被Oracle 收購,它便被融合進OracleJDK之中。在JDK 7 Update 40時開始隨JDK一起釋出,後來Java SE Advanced產品線建立,Oracle明確區分了Oracle OpenJDK和OracleJDK的差別[3],JMC從JDK 11開始又被移除出JDK。雖然在2018年Oracle將JMC開源並交付給OpenJDK組織進行管理,但開源並不意味著 免費使用,JMC需要與HotSpot內部的“飛行記錄儀”(Java Flight Recorder,JFR)配合才能工作,而在 JDK 11以前,JFR的開啟必須解鎖OracleJDK的商業特性支援(使用JCMD的 VM.unlock_commercial_features或啟動時加入-XX:+UnlockCommercialFeatures引數),所以這項功能 在生產環境中仍然是需要付費才能使用的商業特性。

[1] 準確來說是Linux和Solaris在OracleJDK 6就可以使用HSDB和CLHSDB了,Windows上要到Oracle-JDK 7才可以用。
[2] VisualVM官方站點:https://visualvm.github.io。
[3] 詳見https://blogs.oracle.com/java-platform-group/oracle-jdk-releases-for-java-11-and-later。

JHSDB:基於服務性代理的除錯工具

JDK中提供了JCMD和JHSDB兩個整合式的多功能工具箱,它們不僅整合了上一節介紹到的所有基礎工具所能提供的專項功能,而且由於有著“後發優勢”,能夠做得往往比之前的老工具們更好、更 強大,表4-15所示是JCMD、JHSDB與原基礎工具實現相同功能的簡要對比。

表4-15 JCMD、JHSDB和基礎工具的對比

基礎工具JCMDJHSDB
jps -lmjcmdN/A
jmap -dump <pid>jcmd <pid> GC.heap_dumpjhsdb jmap --binaryheap
jmap-histo <pid>jcmd <pid> GC.class_histogramjhsdb jmap --histo
jstack <pid>jcmd <pid> Thread.printjhsdb jstack --locks
jinfo -sysprops <pid>jcmd <pid> VM.system_propertiesjhsdb info --sysprops
jinfo -flags <pid>jcmd <pid> VM.flagsjhsdbj info --flags

JHSDB是一款基於服務性代理(Serviceability Agent,SA)實現的程式外除錯工具。服務性代理是 HotSpot虛擬機器中一組用於對映Java虛擬機器執行資訊的、主要基於Java語言(含少量JNI程式碼)實現的 API集合。服務性代理以HotSpot內部的資料結構為參照物進行設計,把這些C++的資料抽象出Java模型物件,相當於HotSpot的C++程式碼的一個映象。通過服務性代理的API,可以在一個獨立的Java虛擬 機的程式裡分析其他HotSpot虛擬機器的內部資料,或者從HotSpot虛擬機器程式記憶體中dump出來的轉儲快照裡還原出它的執行狀態細節。服務性代理的工作原理跟Linux上的GDB或者Windows上的Windbg是相似的。本次,我們要藉助JHSDB來分析一下程式碼清單4-6中的程式碼[1],並通過實驗來回答一個簡單問題:staticObj、instanceObj、localObj這三個變數本身(而不是它們所指向的物件)存放在哪裡?

程式碼清單4-6 JHSDB測試程式碼

/*** staticObj、instanceObj、localObj存放在哪裡? */ 
public class JHSDB_TestCase { 
	static class Test { 
		static ObjectHolder staticObj = new ObjectHolder(); 
		ObjectHolder instanceObj = new ObjectHolder(); 
		void foo() { 
			ObjectHolder localObj = new ObjectHolder(); 														
			System.out.println("done"); // 這裡設一個斷點 
		} 
	}
	private static class ObjectHolder {} 
	public static void main(String[] args) { 
		Test test = new JHSDB_TestCase.Test(); 
		test.foo(); 
	} 
}

答案讀者當然都知道:staticObj隨著Test的型別資訊存放在方法區,instanceObj隨著Test的物件實 例存放在Java堆,localObject則是存放在foo()方法棧幀的區域性變數表中。現在要做的是通過JHSDB來實踐驗證這一點。 首先,我們要確保這三個變數已經在記憶體中分配好,然後將程式暫停下來,以便有空隙進行實驗,這隻要把斷點設定在程式碼中加粗的列印語句上,然後在除錯模式下執行程式即可。由於JHSDB本 身對壓縮指標的支援存在很多缺陷,建議用64位系統的讀者在實驗時禁用壓縮指標,另外為了後續操作時可以加快在記憶體中搜尋物件的速度,也建議讀者限制一下Java堆的大小。本例中,筆者採用的運 行引數如下:
-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
程式執行後通過jps查詢到測試程式的程式ID,具體如下:

jps -l 

8440 org.jetbrains.jps.cmdline.Launcher
11180 JHSDB_TestCase
15692 jdk.jcmd/sun.tools.jps.Jps
使用以下命令進入JHSDB的圖形化模式,並使其附加程式11180:

jhsdb hsdb --pid 11180

命令開啟的JHSDB的介面如圖4-4所示。
圖4-4 JHSDB的介面
執行至斷點位置一共會建立三個ObjectHolder物件的例項,只要是物件實 例必然會在Java堆中分配,既然我們要查詢引用這三個物件的指標存放在哪裡,不妨從這三個物件開 始著手,先把它們從Java堆中找出來。

首先點選選單中的Tools->Heap Parameters[2],結果如圖4-5所示,因為筆者的執行引數中指定了使 用的是Serial收集器,圖中我們看到了典型的Serial的分代記憶體佈局,Heap Parameters視窗中清楚列出了 新生代的Eden、S1、S2和老年代的容量(單位為位元組)以及它們的虛擬記憶體地址起止範圍。
圖4-5 Serial收集器的堆佈局
如果讀者實踐時不指定收集器,即使用JDK預設的G1的話,得到的資訊應該類似如下所示:
Heap Parameters:
garbage-first heap [0x00007f32c7800000, 0x00007f32c8200000] region size 1024K

請讀者注意一下圖中各個區域的記憶體地址範圍,後面還要用到它們。開啟Windows->Console窗 口,使用scanoops命令在Java堆的新生代(從Eden起始地址到To Survivor結束地址)範圍內查詢 ObjectHolder的例項,結果如下所示:

hsdb>scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_TestCase$ObjectHolder 
0x00007f32c7a7c458 JHSDB_TestCase$ObjectHolder 
0x00007f32c7a7c480 JHSDB_TestCase$ObjectHolder 
0x00007f32c7a7c490 JHSDB_TestCase$ObjectHolder

果然找出了三個例項的地址,而且它們的地址都落到了Eden的範圍之內,算是順帶驗證了一般情©©況下新物件在Eden中建立的分配規則。再使用Tools->Inspector功能確認一下這三個地址中存放的物件,結果如圖4-6所示。
圖4-6 檢視物件例項資料
Inspector為我們展示了物件頭和指向物件後設資料的指標,裡面包括了Java型別的名字、繼承關 系、實現介面關係,欄位資訊、方法資訊、執行時常量池的指標、內嵌的虛方法表(vtable)以及介面方法表(itable)等。由於我們的確沒有在ObjectHolder上定義過任何欄位,所以圖中並沒有看到任何例項欄位資料,讀者在做實驗時不妨定義一些不同資料型別的欄位,觀察它們在HotSpot虛擬機器裡面是如何儲存的。

接下來要根據堆中物件例項地址找出引用它們的指標,原本JHSDB的Tools選單中有Compute Reverse Ptrs來完成這個功能,但在筆者的執行環境中一點選它就出現Swing的介面異常,看後臺日誌是報了個空指標,這個問題只是介面層的異常,跟虛擬機器關係不大,所以筆者沒有繼續去深究,改為使用命令來做也很簡單,先拿第一個物件來試試看:

hsdb> revptrs 0x00007f32c7a7c458 
Computing reverse pointers... 
Done. 
Oop for java/lang/Class @ 0x00007f32c7a7b180 

果然找到了一個引用該物件的地方,是在一個java.lang.Class的例項裡,並且給出了這個例項的地 址,通過Inspector檢視該物件例項,可以清楚看到這確實是一個java.lang.Class型別的物件例項,裡面 有一個名為staticObj的例項欄位,如圖4-7所示。
圖4-7 Class物件
從《Java虛擬機器規範》所定義的概念模型來看,所有Class相關的資訊都應該存放在方法區之中, 但方法區該如何實現,《Java虛擬機器規範》並未做出規定,這就成了一件允許不同虛擬機器自己靈活把握的事情。JDK 7及其以後版本的HotSpot虛擬機器選擇把靜態變數與型別在Java語言一端的對映Class對 象存放在一起,儲存於Java堆之中,從我們的實驗中也明確驗證了這一點[3]。接下來繼續查詢第二個 物件例項:

 hsdb>revptrs 0x00007f32c7a7c480 
 Computing reverse pointers... 
 Done. Oop for JHSDB_TestCase$Test @ 0x00007f32c7a7c468

這次找到一個型別為JHSDB_TestCase$Test的物件例項,在Inspector中該物件例項顯示如圖4-8所示。
圖4-8

這個結果完全符合我們的預期,第二個ObjectHolder的指標是在Java堆中JHSDB_TestCase$Test物件的instanceObj欄位上。但是我們採用相同方法查詢第三個ObjectHolder例項時,JHSDB返回了一個 null,表示未查詢到任何結果:

hsdb> revptrs 0x00007f32c7a7c490 
null 

看來revptrs命令並不支援查詢棧上的指標引用,不過沒有關係,得益於我們測試程式碼足夠簡潔, 人工也可以來完成這件事情。在Java Thread視窗選中main執行緒後點選Stack Memory按鈕檢視該執行緒的棧記憶體,如圖4-9所示。
圖4-9 main執行緒的棧記憶體
這個執行緒只有兩個方法棧幀,儘管沒有查詢功能,但通過肉眼觀察在地址0x00007f32e771c998上的值正好就是0x00007f32c7a7c490,而且JHSDB在旁邊已經自動生成註釋,說明這裡確實是引用了一
個來自新生代的JHSDB_TestCase$ObjectHolder物件。

[1] 本小節的原始案例來自RednaxelaFX的部落格https://rednaxelafx.iteye.com/blog/1847971。
[2] 效果與在Windows->Console中輸入universe命令是等價的,JHSDB的圖形介面中所有操作都可以通 過命令列完成,讀者感興趣的話,可以在控制檯中輸入help命令檢視更多資訊。
[3] 在JDK 7以前,即還沒有開始“去永久代”行動時,這些靜態變數是存放在永久代上的,JDK 7起把 靜態變數、字元常量這些從永久代移除出去。

JConsole:Java監視與管理控制檯

JConsole(Java Monitoring and Management Console)是一款基於JMX(Java Manage-ment Extensions)的視覺化監視、管理工具。它的主要功能是通過JMX的MBean(Managed Bean)對系統進行資訊收集和引數動態調整。JMX是一種開放性的技術,不僅可以用在虛擬機器本身的管理上,還可以執行於虛擬機器之上的軟體中,典型的如中介軟體大多也基於JMX來實現管理與監控。虛擬機器對JMX MBean的訪問也是完全開放的,可以使用程式碼呼叫API、支援JMX協議的管理控制檯,或者其他符合JMX規範的軟體進行訪問。

1.啟動JConsole

通過JDK/bin目錄下的jconsole.exe啟動JCon-sole後,會自動搜尋出本機執行的所有虛擬機器程式,而
不需要使用者自己使用jps來查詢。雙擊選擇其中一個程式便可進入主介面開始監控。 JMX支援跨伺服器的管理,也可以使用下面的“遠端程式”功能來連線遠端伺服器,對遠端虛擬機器進行監控。這裡MonitoringTest是筆者準備的“反面教材”程式碼之一。雙擊它進入JConsole主介面,可以看到主介面裡共包括“概述”、“記憶體”、“執行緒”、“類”、“VM摘要”、“MBean”六個頁籤,如圖4-11所示。
圖4-11 JConsole主介面
“概述”頁籤裡顯示的是整個虛擬機器主要執行資料的概覽資訊,包括“堆記憶體使用情況”、“執行緒”、“類”、“CPU使用情況”四項資訊的曲線圖,這些曲線圖是後面“記憶體”、“執行緒”、“類”頁籤的資訊彙總,具體內容將在稍後介紹。

2.記憶體監控

“記憶體”頁籤的作用相當於視覺化的jstat命令,用於監視被收集器管理的虛擬機器記憶體(被收集器直 接管理的Java堆和被間接管理的方法區)的變化趨勢。我們通過執行程式碼清單4-7中的程式碼來體驗一下 它的監視功能。
執行時設定的虛擬機器引數為: -Xms100m -Xmx100m -XX:+UseSerialGC

程式碼清單4-7 JConsole監視程式碼

/*** 記憶體佔位符物件,一個OOMObject大約佔64KB */ 
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); 
} 

這段程式碼的作用是以64KB/50ms的速度向Java堆中填充資料,一共填充1000次,使用JConsole 的“記憶體”頁籤進行監視,觀察曲線和柱狀指示圖的變化。 程式執行後,在“記憶體”頁籤中可以看到記憶體池Eden區的執行趨勢呈現折線狀,如圖4-12所示。
在這裡插入圖片描述
監視範圍擴大至整個堆後,會發現曲線是一直平滑向上增長的。從柱狀圖可以看到,在1000次迴圈執行結束,執行了System.gc()後,雖然整個新生代Eden和Survivor區都基本被清空了,但是代表老年代的柱狀圖仍然保持峰值狀態,說明被填充進堆中的資料在System.gc()方法執行之後仍然存活。

筆者的分析就到此為止,提兩個小問題供讀者思考一下,答案稍後公佈。
1)虛擬機器啟動引數只限制了Java堆為100MB,但沒有明確使用-Xmn引數指定新生代大小,讀者能否從監控圖中估算出新生代的容量?
2)為何執行了System.gc()之後,圖4-12中代表老年代的柱狀圖仍然顯示峰值狀態,程式碼需要如何調整才能讓System.gc()回收掉填充到堆中的物件?

問題1答案:圖4-12顯示Eden空間為27328KB,因為沒有設定-XX:SurvivorRadio引數,所以Eden 與Survivor空間比例的預設值為8∶1,因此整個新生代空間大約為27328KB×125%=34160KB。
問題2答案:執行System.gc()之後,空間未能回收是因為List<OOMObject>list物件仍然存活, fillHeap()方法仍然沒有退出,因此list物件在System.gc()執行時仍然處於作用域之內[1]。如果把 System.gc()移動到fillHeap()方法外呼叫就可以回收掉全部記憶體。

3.執行緒監控

如果說JConsole的“記憶體”頁籤相當於視覺化的jstat命令的話,那“執行緒”頁籤的功能就相當於視覺化 的jstack命令了,遇到執行緒停頓的時候可以使用這個頁籤的功能進行分析。前面講解jstack命令時提到執行緒長時間停頓的主要原因有等待外部資源(資料庫連線、網路資源、裝置資源等)、死迴圈、鎖等 待等,程式碼清單4-8將分別演示這幾種情況。

程式碼清單4-8 執行緒等待演示程式碼

/*** 執行緒死迴圈演示 */ 
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執行緒,如圖4-13所示。堆疊追蹤顯示BufferedReader的 readBytes()方法正在等待System.in的鍵盤輸入,這時候執行緒為Runnable狀態,Runnable狀態的執行緒仍會被分配執行時間,但readBytes()方法檢查到流沒有更新就會立刻歸還執行令牌給作業系統,這種等待只消耗很小的處理器資源。
圖4-13 main執行緒
接著監控testBusyThread執行緒,如圖4-14所示。testBusyThread執行緒一直在執行空迴圈,從堆疊追蹤中看到一直在MonitoringTest.java程式碼的41行停留,41行的程式碼為while(true)。這時候執行緒為Runnable 狀態,而且沒有歸還執行緒執行令牌的動作,所以會在空迴圈耗盡作業系統分配給它的執行時間,直到執行緒切換為止,這種等待會消耗大量的處理器資源。
圖4-14 testBusyThread執行緒
圖4-15顯示testLockThread執行緒在等待lock物件的notify()或notifyAll()方法的出現,執行緒這時候處於 WAITING狀態,在重新喚醒前不會被分配執行時間。
圖4-15 testLockThread執行緒
testLockThread執行緒正處於正常的活鎖等待中,只要lock物件的notify()或notifyAll()方法被呼叫, 這個執行緒便能啟用繼續執行。程式碼清單4-9演示了一個無法再被啟用的死鎖等待。
程式碼清單4-9 死鎖程式碼樣例

/*** 執行緒死鎖等待演示 */ 
static class SynAddRunalbe implements Runnable { 
	int a, b; 
	public SynAddRunalbe(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++) { 
		new Thread(new SynAddRunalbe(1, 2)).start(); 
		new Thread(new SynAddRunalbe(2, 1)).start(); 
	} 
} 

這段程式碼開了200個執行緒去分別計算1+2以及2+1的值,理論上for迴圈都是可省略的,兩個執行緒也可能會導致死鎖,不過那樣概率太小,需要嘗試執行很多次才能看到死鎖的效果。如果運氣不是特別差的話,上面帶for迴圈的版本最多執行兩三次就會遇到執行緒死鎖,程式無法結束。造成死鎖的根本原因是Integer.valueOf()方法出於減少物件建立次數和節省記憶體的考慮,會對數值為-128~127之間的 Integer物件進行快取[2],如果valueOf()方法傳入的引數在這個範圍之內,就直接返回快取中的物件。 也就是說程式碼中儘管呼叫了200次Integer.valueOf()方法,但一共只返回了兩個不同的Integer物件。假如某個執行緒的兩個synchronized塊之間發生了一次執行緒切換,那就會出現執行緒A在等待被執行緒B持有的 Integer.valueOf(1),執行緒B又在等待被執行緒A持有的Integer.valueOf(2),結果大家都跑不下去的情況。 出現執行緒死鎖之後,點選JConsole執行緒皮膚的“檢測到死鎖”按鈕,將出現一個新的“死鎖”頁籤,如圖4-16所示。
圖4-16 執行緒死鎖
圖4-16中很清晰地顯示,執行緒Thread-43在等待一個被執行緒Thread-12持有的Integer物件,而點選執行緒Thread-12則顯示它也在等待一個被執行緒Thread-43持有的Integer物件,這樣兩個執行緒就互相卡住,除 非犧牲其中一個,否則死鎖無法釋放。

[1] 準確地說,只有虛擬機器使用直譯器執行的時候,“在作用域之內”才能保證它不會被回收,因為這裡的回收還涉及區域性變數表變數槽的複用、即時編譯器介入時機等問題,具體讀者可參考第8章的程式碼清 單8-1。
[2] 這是《Java虛擬機器規範》中明確要求快取的預設值,實際值可以調整,具體取決於java.lang.Integer.Integer-Cache.high引數的設定。

VisualVM:多合-故障處理工具

VisualVM(All-in-One Java Troubleshooting Tool)是功能最強大的執行監視和故障處理程式之一, 曾經在很長一段時間內是Oracle官方主力發展的虛擬機器故障處理工具。Oracle曾在VisualVM的軟體說明中寫上了“All-in-One”的字樣,預示著它除了常規的執行監視、故障處理外,還將提供其他方面的能力,譬如效能分析(Profiling)。VisualVM的效能分析功能比起JProfiler、YourKit等專業且收費的 Profiling工具都不遑多讓。而且相比這些第三方工具,VisualVM還有一個很大的優點:不需要被監視的程式基於特殊Agent去執行,因此它的通用性很強,對應用程式實際效能的影響也較小,使得它可以直接應用在生產環境中。這個優點是JProfiler、YourKit等工具無法與之媲美的。

1.VisualVM相容範圍與外掛安裝

VisualVM基於NetBeans平臺開發工具,所以一開始它就具備了通過外掛擴充套件功能的能力,有了外掛擴充套件支援,VisualVM可以做到:

  • 顯示虛擬機器程式以及程式的配置、環境資訊(jps、jinfo)。
  • 監視應用程式的處理器、垃圾收集、堆、方法區以及執行緒的資訊(jstat、jstack)。
  • dump以及分析堆轉儲快照(jmap、jhat)。
  • 方法級的程式執行效能分析,找出被呼叫最多、執行時間最長的方法。
  • 離執行緒序快照:收集程式的執行時配置、執行緒dump、記憶體dump等資訊建立一個快照,可以將快 照傳送開發者處進行Bug反饋。
  • 其他外掛帶來的無限可能性。

VisualVM在JDK 6 Update 7中首次釋出,但並不意味著它只能監控執行於JDK 6上的程式,它具備很優秀的向下相容性,甚至能向下相容至2003年釋出的JDK 1.4.2版本[1],這對無數處於已經完成實施、正在維護的遺留專案很有意義。當然,也並非所有功能都能完美地向下相容,主要功能的相容性 見表4-16所示。

特性JDK 1.4.2JDK 5JDK 6 localJDK 6 remote
執行環境資訊
系統屬性
監視皮膚
執行緒皮膚
效能監控
堆、執行緒Dump
MBean管理
JConsole外掛

首次啟動VisualVM後,讀者先不必著急找應用程式進行監測,初始狀態下的VisualVM並沒有載入 任何外掛,雖然基本的監視、執行緒皮膚的功能主程式都以預設外掛的形式提供,但是如果不在VisualVM上裝任何擴充套件外掛,就相當於放棄它最精華的功能,和沒有安裝任何應用軟體的作業系統差不多。

VisualVM的外掛可以手工進行安裝,在網站[2]上下載nbm包後,點選“工具->外掛->已下載”菜 單,然後在彈出對話方塊中指定nbm包路徑便可完成安裝。獨立安裝的外掛儲存在VisualVM的根目錄,譬如JDK 9之前自帶的VisulalVM,外掛安裝後是放在JDK_HOME/lib/visualvm中的。手工安裝外掛並不常用,VisualVM的自動安裝功能已可找到大多數所需的外掛,在有網路連線的環境下,點選“工具-> 外掛選單”,彈出如圖4-17所示的外掛頁籤,在頁籤的“可用外掛”及“已安裝”中列舉了當前版本 VisualVM可以使用的全部外掛,選中外掛後在右邊視窗會顯示這個外掛的基本資訊,如開發者、版 本、功能描述等。
圖4-17 VisualVM外掛頁籤
圖4-19 VisualVM主介面

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

在VisualVM中生成堆轉儲快照檔案有兩種方式,可以執行下列任一操作:

  • 在“應用程式”視窗中右鍵單擊應用程式節點,然後選擇“堆Dump”。
  • 在“應用程式”視窗中雙擊應用程式節點以開啟應用程式標籤,然後在“監視”標籤中單擊“堆Dump”。

生成堆轉儲快照檔案之後,應用程式頁籤會在該堆的應用程式下增加一個以[heap-dump]開頭的子節點,並且在主頁籤中開啟該轉儲快照,如圖4-20所示。如果需要把堆轉儲快照儲存或傳送出去,就應在heapdump節點上右鍵選擇“另存為”選單,否則當VisualVM關閉時,生成的堆轉儲快照檔案會被當作臨時檔案自動清理掉。要開啟一個由已經存在的堆轉儲快照檔案,通過檔案選單中的“裝入”功能,選擇硬碟上的檔案即可。
圖4-20 瀏覽dump檔案
堆頁籤中的“摘要”皮膚可以看到應用程式dump時的執行時引數、System.getProperties()的內容、 執行緒堆疊等資訊;“類”皮膚則是以類為統計口徑統計類的例項數量、容量資訊;“例項”皮膚不能直接使用,因為VisualVM在此時還無法確定使用者想檢視哪個類的例項,所以需要通過“類”皮膚進入, 在“類”中選擇一個需要檢視的類,然後雙擊即可在“例項”裡面看到此類的其中500個例項的具體屬性信 息;“OQL控制檯”皮膚則是執行OQL查詢語句的,同jhat中介紹的OQL功能一樣。如果讀者想要了解具體OQL的語法和使用方法,可參見本書附錄D的內容。

3.分析程式效能

在Profiler頁籤中,VisualVM提供了程式執行期間方法級的處理器執行時間分析以及記憶體分析。做 Profiling分析肯定會對程式執行效能有比較大的影響,所以一般不在生產環境使用這項功能,或者改用 JMC來完成,JMC的Profiling能力更強,對應用的影響非常輕微。

要開始效能分析,先選擇“CPU”和“記憶體”按鈕中的一個,然後切換到應用程式中對程式進行操作,VisualVM會記錄這段時間中應用程式執行過的所有方法。如果是進行處理器執行時間分析,將會統計每個方法的執行次數、執行耗時;如果是記憶體分析,則會統計每個方法關聯的物件數以及這些物件所佔的空間。等要分析的操作執行結束後,點選“停止”按鈕結束監控過程,如圖4-21所示。
圖4-21 對應用程式進行CPU執行時間分析

注意 在JDK 5之後,在客戶端模式下的虛擬機器加入並且自動開啟了類共享——這是一個在多虛擬機器程式共享rt.jar中類資料以提高載入速度和節省記憶體的優化,而根據相關Bug報告的反映, VisualVM的Profiler功能會因為類共享而導致被監視的應用程式崩潰,所以讀者進行Profiling前,最好在被監視程式中使用-Xshare:off引數來關閉類共享優化。

4.BTrace動態日誌跟蹤

BTrace[3]是一個很神奇的VisualVM外掛,它本身也是一個可執行的獨立程式。BTrace的作用是在不中斷目標程式執行的前提下,通過HotSpot虛擬機器的Instrument功能[4]動態加入原本並不存在的除錯程式碼。這項功能對實際生產中的程式很有意義:如當程式出現問題時,排查錯誤的一些必要資訊時(譬如方法引數、返回值等),在開發時並沒有列印到日誌之中以至於不得不停掉服務時,都可以通過除錯增量來加入日誌程式碼以解決問題。

在VisualVM中安裝了BTrace外掛後,在應用程式皮膚中右擊要除錯的程式,會出現“Trace Application…”選單,點選將進入BTrace皮膚。這個皮膚看起來就像一個簡單的Java程式開發環境,裡面甚至已經有了一小段Java程式碼,如圖4-22所示。
圖4-22 BTrace動態跟蹤
筆者準備了一段簡單的Java程式碼來演示BTrace的功能:產生兩個1000以內的隨機整數,輸出這兩 個數字相加的結果,如程式碼清單4-10所示。
程式碼清單4-10 BTrace跟蹤演示

public class BTraceTest { 
	public int add(int a, int b) { return a + b; }
	public static void main(String[] args) throws IOException { 
		BTraceTest test = new BTraceTest(); 
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 
		for (int i = 0; i < 10; i++) { 
			reader.readLine(); 
			int a = (int) Math.round(Math.random() * 1000); 
			int b = (int) Math.round(Math.random() * 1000); 
			System.out.println(test.add(a, b)); 
		} 
	}
}

假設這段程式已經上線執行,而我們現在又有了新的需求,想要知道程式中生成的兩個隨機數是什麼,但程式並沒有在執行過程中輸出這一點。此時,在VisualVM中開啟該程式的監視,在BTrace頁籤填充TracingScript的內容,輸入除錯程式碼,如程式碼清單4-11所示,即可在不中斷程式執行的情況下做到這一點。
程式碼清單4-11 BTrace除錯程式碼

/* BTrace Script Template */ 
import com.sun.btrace.annotations.*; 
import static com.sun.btrace.BTraceUtils.*; 
@BTrace 
public class TracingScript { 
	@OnMethod( clazz="org.fenixsoft.monitoring.BTraceTest", method="add", location=@Location(Kind.RETURN) )
	public static void func(@Self org.fenixsoft.monitoring.BTraceTest instance,int a, int b,@Return int result) { 
		println("呼叫堆疊:"); 
		jstack(); 
		println(strcat("方法引數A:",str(a))); 
		println(strcat("方法引數B:",str(b))); 
		println(strcat("方法結果:",str(result))); 
	}
} 

點選Start按鈕後稍等片刻,編譯完成後,Output皮膚中會出現“BTrace code successfuly deployed”的字樣。當程式執行時將會在Output皮膚輸出如圖4-23所示的除錯資訊。
圖4-23 BTrace跟蹤結果
BTrace的用途很廣泛,列印呼叫堆疊、引數、返回值只是它最基礎的使用形式,在它的網站上有使用BTrace進行效能監視、定位連線洩漏、記憶體洩漏、解決多執行緒競爭問題等的使用案例,有興趣的讀者可以去網上了解相關資訊。

BTrace能夠實現動態修改程式行為,是因為它是基於Java虛擬機器的Instrument開發的。Instrument是Java虛擬機器工具介面(Java Virtual Machine Tool Interface,JVMTI)的重要元件,提供了一套代理 (Agent)機制,使得第三方工具程式可以以代理的方式訪問和修改Java虛擬機器內部的資料。阿里巴巴開源的診斷工具Arthas也通過Instrument實現了與BTrace類似的功能。

[1] 早於JDK 6的平臺,需要開啟-Dcom.sun.management.jmxremote引數才能被VisualVM管理。
[2] 外掛中心地址:https://visualvm.github.io/pluginscenters.html。
[3] 官方主頁:https://github.com/btraceio/btrace。
[4] 是JVMTI中的主要組成部分,HotSpot虛擬機器允許在不停止執行的情況下,更新已經載入的類的程式碼。

Java Mission Control:可持續線上的監控工具

除了大家熟知的面向通用計算(General Purpose Computing)可免費使用的Java SE外,Oracle公司 還開闢過帶商業技術支援的Oracle Java SE Support和麵向獨立軟體供應商(ISV)的Oracle Java SE Advanced & Suite產品線。

除去帶有7×24小時的技術支援以及可以為企業專門定製安裝包這些非技術類的增強服務外, Oracle Java SE Advanced & Suite[1]與普通Oracle Java SE在功能上的主要差別是前者包含了一系列的監控、管理工具,譬如用於企業JRE定製管理的AMC(Java Advanced Management Console)控制檯、 JUT(Java Usage Tracker)跟蹤系統,用於持續收集資料的JFR(Java Flight Recorder)飛行記錄儀和用 於監控Java虛擬機器的JMC(Java Mission Control)。這些功能全部都是需要商業授權才能在生產環境中 使用,但根據Oracle Binary Code協議,在個人開發環境中,允許免費使用JMC和JFR,本節筆者將簡要介紹它們的原理和使用。

JFR是一套內建在HotSpot虛擬機器裡面的監控和基於事件的資訊蒐集框架,與其他的監控工具(如 JProfiling)相比,Oracle特別強調它“可持續線上”(Always-On)的特性。JFR在生產環境中對吞吐量的影響一般不會高於1%(甚至號稱是Zero Performance Overhead),而且JFR監控過程的開始、停止都是完全可動態的,即不需要重啟應用。JFR的監控對應用也是完全透明的,即不需要對應用程式的原始碼做任何修改,或者基於特定的代理來執行。

JMC最初是BEA公司的產品,因此並沒有像VisualVM那樣一開始就基於自家的Net-Beans平臺來開發,而是選擇了由IBM捐贈的Eclipse RCP作為基礎框架,現在的JMC不僅可以下載到獨立程式,更常見的是作為Eclipse的外掛來使用。JMC與虛擬機器之間同樣採取JMX協議進行通訊,JMC一方面作為 JMX控制檯,顯示來自虛擬機器MBean提供的資料;另一方面作為JFR的分析工具,展示來自JFR的資料。啟動後JMC的主介面如圖4-24所示。
圖4-24 JMC主介面

相關文章