本文將簡要介紹Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工做中融會貫通。php
Java VisualVM是JDK1.6後自帶的可視化工具,提供圖形界面以實時監控應用程序的線程狀態、CPU和內存資源消耗狀況,而且能夠保存快照以便脫機分析程序的性能瓶頸。java
JDK1.6以後已自帶VisualVM工具(jvisualvm.exe)。若使用非Oracle JDK,可自行登陸官網下載VisualVM並安裝。
工具下載後,須要在visualvm_143\etc\visualvm.conf
裏手工配置JDK路徑(visualvm_jdkhome)。git
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(不啓用用戶認證)
github
而後重啓遠程程序。此時,經過netstat -ano | findstr 8090
(Windows)或netstat -anlp | grep 8090
(Linux)查看端口已處於Listening狀態,代表能夠進行遠程JMX鏈接。express
除單獨使用VisualVM工具外,也可在IDEA中集成VisualVM launcher插件。經過File-> Setting-> Plugins -> Browers Repositrories搜索VisualVM Launcher安裝並重啓IDEA後,會出現菜單和按鈕兩種啓動方式:
數組
點擊按鈕後會出現選擇VisualVM路徑,選擇VisualVM可執行文件便可。此後,點擊啓動會打開一個VisualVM窗口。服務器
本節結合代碼示例介紹VisualVM的界面功能。示例代碼以下:eclipse
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(Memory Analyzer Tool)是一個快速、功能豐富的JAVA堆轉儲文件分析工具,可幫助開發者發現內存泄漏和減小內存消耗。
MAT常見的使用場景以下:
從官網下載單機版MAT工具,解壓後直接運行MAT目錄的MemoryAnalyzer.exe便可啓動MAT。
若待分析的dump文件過大,可增大安裝目錄下MemoryAnalyzer.ini文件裏的Xmx參數值(默認1G)。注意,Xmx取值不能大於運行環境的的系統內存,不然MAT啓動時會報錯Failed to create the Java Virtual Machine
。
MAT是一個靜態堆分析工具,須要預先抓取Java堆轉儲文件(內存快照)。
可經過如下幾種方式生成堆轉儲文件:
1) 在JVM啓動參數裏增長-XX:+HeapDumpOnOutOfMemoryError
參數,系統發生OOM時會自動在工做目錄(user.dir)生成java_pid<pid>.hprof
轉儲文件。還可經過JVM參數-XX:HeapDumpPath=<path>
顯式指定堆轉儲文件的存放路徑。
2) 若是不想等到發生OOM錯誤時纔得到堆轉儲文件,可添加JVM參數-XX:+HeapDumpOnCtrlBreak
,以便在控制檯使用Ctrl+Break(Pause)鍵來按需獲取堆轉儲文件。
3) 若環境上Jmap工具可用,則可經過jmap -dump:live,format=b,file=heap.bin <pid>
命令得到轉儲文件。
其中,pid爲進程ID,live選項會在轉儲前強制觸發一次full GC(以減少文件體積),file可指定產生文件的目錄和名稱。
相似地,VisualVM、Jconsole等JDK工具也可用來生成堆轉儲文件。
4) 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操做步驟以下:
1) 加載第一個堆轉儲文件,並打開Histogram視圖。
2) 打開Window -> Navigation History視圖,在histogram右鍵選擇Add to Compare Basket。
3) 加載第二個堆轉儲文件,也添加到Compare Basket中。
4) 打開Window -> Compare Basket視圖,點擊Compare the Results(右上角的紅色歎號)。
5) 在Compared Tables裏分析對比結果。
例如,圖中#1是使用String.intern()存儲密碼後的內存信息,比#0建立的String對象要少(這由OQL結果也可證實)。
經過這種方式可快速定位到操做先後所持有的對象增量,從而進一步定位出致使內存泄露的具體元兇。
本文簡要介紹了Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工做中融會貫通。真是無話可說了……