前言
本文將簡要介紹Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工作中融會貫通。
VisualVM
Java VisualVM是JDK1.6后自帶的可視化工具,提供圖形界面以實時監控應用程序的線程狀態、CPU和內存資源消耗情況,並且可以保存快照以便脫機分析程序的性能瓶頸。
安裝與配置
JDK1.6之后已自帶VisualVM工具(jvisualvm.exe)。若使用非Oracle JDK,可自行登錄官網下載VisualVM並安裝。
工具下載后,需要在visualvm_143\etc\visualvm.conf
里手工配置JDK路徑(visualvm_jdkhome)。
VisualVM可監控本地或遠程的Java程序。使用遠程監控時需要在服務端啟動JMX服務。首先,在遠程程序的啟動參數中增加如下JVM參數:
-Djava.rmi.server.hostname=10.186.189.98(遠程服務器IP地址) -Dcom.sun.management.jmxremote.port=8090(JMX遠程監聽端口) -Dcom.sun.management.jmxremote.ssl=false(禁用SSL) -Dcom.sun.management.jmxremote.authenticate=false(不啟用用戶認證)
然后重啟遠程程序。此時,通過netstat -ano | findstr 8090
(Windows)或netstat -anlp | grep 8090
(Linux)查看端口已處於Listening狀態,表明可以進行遠程JMX連接。
除單獨使用VisualVM工具外,也可在IDEA中集成VisualVM launcher插件。通過File-> Setting-> Plugins -> Browers Repositrories搜索VisualVM Launcher安裝並重啟IDEA后,會出現菜單和按鈕兩種啟動方式:
點擊按鈕后會出現選擇VisualVM路徑,選擇VisualVM可執行文件即可。此后,點擊啟動會打開一個VisualVM窗口。
本地使用
本節結合代碼示例介紹VisualVM的界面功能。示例代碼如下:
package thread;
public class InfiniteLoop {
public static void main(String[] args) {
Thread t1 = new Thread(new ImplicitLoop(), "ImplicitLoop");
Thread t2 = new Thread(new ExplicitLoop(),"ExplicitLoop");
t1.start();
t2.start();
}
}
class ExplicitLoop extends Thread {
@Override
public void run() {
while (true) {
System.out.println("I work hard!");
}
}
}
class ImplicitLoop extends Thread {
@Override
public void run() {
for (byte i = 0; i < 150; i += 2) { //此處因數值溢出導致死循環
System.out.println("I've worked " + i + " hours!");
if (i >= 120) {
try {
System.out.println("I'll take a short break...");
Thread.sleep(20);
System.out.println("I wake up!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
啟動VisualVM查看本地監控信息,界面如下:
左側"本地(Local)"下列出包含InfiniteLoop示例在內的本地Java進程,右側以概述(Overview)、監視(Monitor)、線程(Threads)、抽樣器(Sampler)等頁簽展示出詳細信息。
其中,概述頁可查看進程的基本信息、JVM啟動參數、系統屬性(同jinfo -sysprops <pid>
)等信息。
監視頁可查看CPU、內存(堆與元空間)、類和線程的的實時折線圖。執行垃圾回收(Perform GC)按鈕可以觸發系統GC,堆Dump(Heap Dump)按鈕可在指定目錄生成堆轉儲(Dump)文件。
注意,本地監控時點擊堆Dump(Heap Dump)按鈕會自動加載打開生成的dump文件,而遠程監控時需要將遠程主機上生成的dump文件拷貝至本地再手工加載。此外,VisualVM加載分析內存Dump文件時非常緩慢,建議使用MAT來分析內存Dump。
線程頁可詳細查看每個線程的運行時間及狀態。線程Dump(Thread Dump)按鈕可生成線程dump文件(類jstack <pid>
)。
圖中,時間線里展示活動線程的運行、休眠(sleep)、等待(o.wait)、駐留(空閑)和監視(同步阻塞)狀態,並可通過縮放按鈕更細致地觀察線程狀態。
Threads inspector
插件可展示單個或多個線程的堆棧。圖中僅勾選了ImplicitLoop線程,由堆棧可知其阻塞在System.out.println("I've worked " + i + " hours!")
行——執行該方法會先加鎖!通過Refresh按鈕刷新堆棧,會發現ImplicitLoop線程有時會處於休眠狀態。
抽樣器頁以一定的時間間隔對CPU、內存進行采樣,可檢查出占用CPU時間較多或占用內存空間較大的線程,有助於性能調優。對CPU采樣時,該頁提供CPU樣例(CPU samples)和線程CPU時間(Thread CPU time)兩個子頁簽,前者可用於分析調用鏈上的方法耗時,后者可用於比較線程CPU耗時。
VisualVM還提供不少有用的插件,例如Visual GC(查看垃圾回收的狀態)。可通過工具(Tools) -> 插件(Plugins)下載插件。
遠程監控
在VisualVM左側點擊遠程(Remote) -> 添加遠程主機(Add Remote Host),填寫服務器IP地址。
然后點擊遠程主機,右鍵"添加JMX連接(Add JMX Connection)",填寫JMX端口號並勾選"不要求SSL連接(Do not require SSL)"。
在添加的JMX連接上右鍵"打開(Open)"或直接雙擊,在界面右側可看到監控面板。
MAT
MAT(Memory Analyzer Tool)是一個快速、功能豐富的JAVA堆轉儲文件分析工具,可幫助開發者發現內存泄漏和減少內存消耗。
使用場景
MAT常見的使用場景如下:
- OOM(OutOfMemoryError異常),原因通常有:
- 對象已死但無法通過垃圾收集器自動回收,內存不斷泄露——需找到泄露的代碼位置並加以修復
- 產生大量生命周期太長或持有狀態時間過長的對象——除增大堆分配空間外,考慮優化存儲結構或代碼邏輯
- CPU負載沖高、線程死鎖等(類似VisualVM)
- 窺探內存對象的內容,例如:
- 排障時環境不允許進行Debug調試或添加日志打印
- 掃描內存中是否存在常駐的明文口令等敏感信息
安裝與配置
從官網下載單機版MAT工具,解壓后直接運行MAT目錄的MemoryAnalyzer.exe即可啟動MAT。
若待分析的dump文件過大,可增大安裝目錄下MemoryAnalyzer.ini文件里的Xmx參數值(默認1G)。注意,Xmx取值不能大於運行環境的的系統內存,否則MAT啟動時會報錯Failed to create the Java Virtual Machine
。
獲得堆轉儲文件
MAT是一個靜態堆分析工具,需要預先抓取Java堆轉儲文件(內存快照)。
可通過以下幾種方式生成堆轉儲文件:
- 在JVM啟動參數里增加
-XX:+HeapDumpOnOutOfMemoryError
參數,系統發生OOM時會自動在工作目錄(user.dir)生成java_pid<pid>.hprof
轉儲文件。還可通過JVM參數-XX:HeapDumpPath=<path>
顯式指定堆轉儲文件的存放路徑。 - 如果不想等到發生OOM錯誤時才獲得堆轉儲文件,可添加JVM參數
-XX:+HeapDumpOnCtrlBreak
,以便在控制台使用Ctrl+Break(Pause)鍵來按需獲取堆轉儲文件。 - 若環境上Jmap工具可用,則可通過
jmap -dump:live,format=b,file=heap.bin <pid>
命令獲得轉儲文件。
其中,pid為進程ID,live選項會在轉儲前強制觸發一次full GC(以減小文件體積),file可指定產生文件的目錄和名稱。
類似地,VisualVM、Jconsole等JDK工具也可用來生成堆轉儲文件。 - MAT本身也可獲取堆轉儲文件,即File -> Acquire Heap Dump菜單。
分析堆轉儲文件
本節亦結合代碼示例介紹MAT常見的界面功能。示例代碼如下:
package thread;
import java.util.*;
public class JavaHeapDump {
private static List<String> smallArray = new ArrayList<>();
private static List<byte[]> largeArray = new ArrayList<>();
public static String getPassword() {
char[] pw = {'A', 'd', 'm', 'i', 'n', '1', '2', '3'};
return new String(pw);
}
public static void makeHeapOom() {
for (int i = 0; i < 1000; i++) {
smallArray.add(getPassword()); //smallArray.add(getPassword().intern());
byte[] elems = new byte[1024 * 1024];
Arrays.fill(elems, (byte)101);
largeArray.add(elems);
//largeArray.add(new byte[1024 * 1024]);
}
}
public static void main(String[] args) {
makeHeapOom();
}
}
編譯代碼后以-Xms20m -Xmx20m
(限制堆空間以盡快OOM)等JVM參數運行,得到如下輸出:
D:\xywang\target\classes>java -Xms20m -Xmx20m -Xmn2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\dump\heapDump.bin thread.JavaHeapDum
p
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\dump\heapDump.bin ...
Heap dump file created [21458899 bytes in 0.805 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at thread.JavaHeapDump.makeHeapOom(JavaHeapDump.java:18)
at thread.JavaHeapDump.main(JavaHeapDump.java:26)
可知,很快就出現內存溢出(java.lang.OutOfMemoryError: Java heap space
),並在E盤下生成heapDump.bin轉儲文件。
此時啟動MAT,選擇菜單項File -> Open Heap Dump來加載待分析的堆轉儲文件。加載完文件后,在彈出的向導頁面選擇按照內存泄漏模式分析。
Leak Suspect Report是默認生成的可能存在潛在內存泄露的分析報告,在餅圖中描述了各種問題占用內存的比例,餅圖下方則是關於潛在問題的細節分析。
點擊"Details"鏈接,可看到引起內存溢出可能的最大元凶確實為largeArray
!此處,"Shortest Paths To the Accumulation Point
"展示由於和哪個GC Root相連導致當前Retained Heap占用相當大的對象無法被回收。
概覽頁簽提供了Heap Dump的概覽,包括堆的餅圖以及Actions/Reports/Step by Step等快速訪問功能區。
其中,Histogram(堆直方圖)提供按類分組的對象的內存占用統計列表,默認按照某個類的shallow heap從大到小排序。Dominator Tree(支配樹)顯示按照Object/Class保留內存大小排序的結果,可用於排查哪些對象導致其他對象無法被垃圾收集器回收。Top Consumers是Dominator Tree數據的圖形統計,分別按照Object、Class,ClassLoader,Package等維度做的內存占用統計。Top Components列出占用堆空間較多的組件,並給出可以減少內存消耗的建議。
以最常用的Dominator Tree界面為例:
堆中有兩個ArrayList,且其中一個占用了96.57%的內存。以下簡要介紹圖中主要字段的含義:
Shallow Heap:對象自身所占用的內存大小,不含其引用的對象所占的內存大小。數組對象的Shallow Heap是數組元素大小的總和,非數組對象的Shallow Heap是對象所有成員變量大小的總和。
Retained Heap:當前對象大小 + 當前對象可直接或間接引用到的對象的大小總和,即當前對象被GC后從Heap上總共能釋放掉的內存。
incoming references:當前類被哪些類引用,或當前對象被哪些對象引用。
outgoing references:當前類的所有實例,或當前對象所引用的對象。
選中Dominator Tree中占用內存最大的對象,通過with incoming references
查看持有其引用的外部對象。
可見,占用大量內容的元凶正是largeArray
。對於集合對象,可右鍵選擇Java Collections的子菜單做各種排序和查看。例如,圖中選擇Extract List Values
查看largeArray
的內容,結果如下所示:
窺探對象內存值
JavaHeapDump示例代碼中有意使用到密碼,真實業務中可通過OQL(Object Query Language)排查內存中是否存在此類敏感信息。
OQL是一種基於javascript表達式的語言,它將類當作表、該類的實例對象當作記錄行、對象中的成員變量當作表中的字段,可以用類似SQL語句的方式查詢Java堆中的對象。OQL語法結構如下:
select <JavaScript expression to select>
from [ instanceof ] <class name="name">
[ where <JavaScript boolean expression to filter> ]
更多OOL的語法,請在OOL頁面上按F1鍵查看幫助信息。
在MAT工具欄中點擊OQL按鈕,打開OQL編輯器窗口,輸入查詢命令后點擊紅色感嘆號按鈕進行查詢,結果如下:
注意,查詢語句中"Admin123"后面的".*"相當於SQL通配符"%"。查詢結果中赫然可見"Admin123"這樣的明文密碼!
通過Merge Shortest Path to GC Roots
查看這些密碼對象到GC Roots是否可達:
若該對象為unreachable則說明密碼不是常駐內存,可見圖中的密碼均常駐內存。
堆轉儲文件對比分析
實際業務場景中堆中內存對象可能非常多,定位內存泄露時,通常需要抓取和對比先后兩個時刻的堆轉儲文件。MAT操作步驟如下:
- 加載第一個堆轉儲文件,並打開Histogram視圖。
- 打開Window -> Navigation History視圖,在histogram右鍵選擇Add to Compare Basket。
- 加載第二個堆轉儲文件,也添加到Compare Basket中。
- 打開Window -> Compare Basket視圖,點擊Compare the Results(右上角的紅色嘆號)。
- 在Compared Tables里分析對比結果。
例如,圖中#1是使用String.intern()存儲密碼后的內存信息,比#0創建的String對象要少(這由OQL結果也可證明)。
通過這種方式可快速定位到操作前后所持有的對象增量,從而進一步定位出導致內存泄露的具體元凶。
總結
本文簡要介紹了Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工作中融會貫通。真是無話可說了……