使用jmap和MAT觀察Java程序內存數據
來自: http://www.xinitek.com/blog/2014-06-23_%E4%BD%BF%E7%94%A8jmap%E5%92%8CMAT%E8%A7%82%E5%AF%9FJava%E...
使用jmap和MAT觀察Java程序內存數據
背景
很多故障跟數據結構中實際存儲的數值會很有關系。有時候我們能夠預感到這類數據,例如:
-
某些 成員變量:緩存池的當前尺寸(size)、容積上限(capacity)
為了加快服務響應速度,通常會把一些相對靜態的內容在內存中做緩存。內存中容納這些內容的容器稱為緩存池。緩存池中已存對象的數目,即緩存池的當前尺寸。這個當前尺寸與該緩存池實際占用內存量息息相關。
由于內存有限,緩存池通常會設置容積上限。這個值可以控制緩存池 占用內存量的最大值。 -
某些類的對象數量、某個對象導致的無法回收內存量(retained size)
-
局部變量的值
有經驗的開發人員可能將這些數值打印到日志中,便于故障分析。
但很多時候我們并沒有預先打印這些數值,或者很計算出這些數值(如無法回收內存量)。在沒有日志記錄的情況下,該如何獲得這些數值?
答案是使用jmap 和 MAT(Memory Analyzer Tool)!
工具介紹
- jmap是JDK自帶的命令行工具,用它可以將指定進程的整個Java堆(Java Heap)轉儲到指定的數據文件中
- MAT是一個第三方工具,可以從http://www.eclipse.org/mat/下載。必須在圖形界面(GUI)中執行(例如Windows或Linux的VNC桌面系統中)。用它可以對數據文件做非常詳盡的分析。
它們之間的關系圖示如下:
工具使用
jmap使用方法:
jmap -dump:file=dump.map <pid>
執行后,會生成dump.map文件。這個文件可以用MAT打開。
如果你在遠程Linux系統中工作,可能需要使用VNC(具體方法參考: (TODO) )。
啟動MAT的方法(在Linux桌面中,打開一個終端(Terminal),然后執行):
#假設mat工具已經解壓到/usr/local/mat中 cd /usr/local/mat #如果dump.map文件尺寸較大(例如2GB以上),需要修改MemoryAnalyzer.ini文件中 #-Xmx選項。將它設置到一個更大的值,例如4GB左右: -Xmx4000m #啟動Memory Analyzer ./MemoryAnalyzer
啟動后,在菜單中選擇Open Heap Dump,讀入dump.map文件即可開始分析。
工具說明
jmap生成的數據文件(例如dump.map)中有詳盡的Java堆(Java Heap)的信息,這些信息包括—所有的Java對象的數據成員,以及它們之間的引用關系等。并且MAT又是一個非常棒的工具,因此上述內容都可以看到。并且MAT還為內存泄漏的分析有特別的支持,它能夠計算出每個對象的占用內存量(Retained Size)。將對象按照占用內存量(Retained Size)從大到小排序,通常很容易就能確定究竟是什么對象發生了內存泄漏。
在MAT中打開數據文件(例如dump.map)后,通常可以通過如下幾個方式來查看:
- Leak Suspect: Memory Analyzer智能分析出的最可能發生內存泄漏的對象,通常也就是占用內存量(Retained Size)最大的對象
- Dominator Tree查看,常用的兩種方式:
- 按照占用內存量(Retained Size)從大到小排序(Dominator Tree的默認查看方式)
- 用正則表達式匹配類名,查看對應的類(Class)的對象(Object),或者對應類的靜態成員(Static Member)
- Threads View查看:可以看到各個線程的調用棧,以及局部引用的值
- Object Query Language查詢:可以用類似SQL的語句查詢整個Java Heap堆
使用MAT不但可以觀察(Inspect)所有對象的成員變量的值,如果變量是引用,還可以跟蹤(follow)引用(reference)進一步查看相關對象的數據。
示例
撰寫程序如下(MatExample.java):
import java.util.List; import java.util.LinkedList; public class MatExample { static String staticVar = "Hello, I am static"; private String instanceVar = "Hello, I am instance"; public static void main(String[] args) throws Exception { MatExample instance = new MatExample(); List<String> leakList = new LinkedList<String>(); int k = 0; for(int i=0; i<10000; i++) { k++; leakList.add(k+""); } System.out.println("10000 objects generated"); while(true) { Thread.sleep(1000); } } } 編譯(生成MatExample.class)
javac MatExample.java
運行(MatExample.class)
java MatExample
程序輸出:
1000 objects generated
另開一個命令行,然后使用jps列出所有Java進程:
jps -mlvV
jps輸出類似如下信息:
91888 sun.tools.jps.Jps -mlvV -Dapplication.home=E:\Java\jdk1.7.0_45 -Xms8m 90752 MatExample
上面90752便是MatExample的進程編號(PID),現在我們運行jmap獲取數據文件(存儲到dump.map):
jmap -dump:file=dump.map 90752
接著啟動MAT,并通過菜單中 File->Open Heap Dump 打開dump.map,如圖:
MAT會讀取數據文件到內存中并作必要的預處理(如果是較大的文件,則耗時較長,建議去喝杯茶休息下)。
MAT讀取完文件,會出現一個向導,讓我們選擇一種分析方式。我們可以選擇泄漏嫌疑對象報告(Leak Suspects Report)分析,然后點“Finish”。
MAT呈現出一個餅圖描述該進程內存的分布情況,在此圖形中可以清晰地看到內存占用最大的對象。下方,還會有Top N的泄露嫌疑對象文字描述,如圖:
這里說的是局部變量占用“721,136 (63.90%) bytes”,并且是一個LinkedList對象造成的。點擊“See stacktrace”,就能看到調用棧
main at java.lang.Thread.sleep(J)V (Native Method) at MatExample.main([Ljava/lang/String;)V (MatExample.java:21)
線程名稱是“main”。點擊圖示的按鈕,可以打開線程視圖(Threads view),查看帶有局部變量的調用棧信息。
從中可以清晰看到是在MatExample.main函數中引用的一個LinkedList占用了720,040字節。
我們可以點擊菜單中Window -> Inspector,然后查看該LinkedList對象的屬性。
可見,其size屬性確實是10000。可見泄露嫌疑對象報告(Leak Suspect Report)能夠幫助我們較迅速地定位到內存泄漏點。
另外,也可以從內存占用最大的對象的思路來找內存問題,點擊圖示圖標,打開Dominator Tree View
將最上面的一行逐行展開,也可以看到LinkedList的內存占用較大。
下面嘗試查看指定類型(class)的靜態成員(static member)和對象
在Dominator Tree的正則輸入框(Regex)中輸入“MatExample”:
回車后,顯示下圖:
“class MatExample”的屬性和引用,與代碼中定義的MatExample類的靜態成員(static member)的內容相符合:
static String staticVar = "Hello, I am static";
我們可以嘗試下跟蹤引用:在Inspector窗口中,對一個引用點擊右鍵,然后選擇“Go Into”
即可看到該對象的屬性。
如果希望讓引用對象顯示在右圖中,可以選擇"List objects -> with outgoing references”
這樣可以更方便地進一步跟蹤其引用。
但我們發現,代碼中創建的MatExample對象
MatExample instance = new MatExample();
并沒有出現在之前的圖中
可以嘗試下Object Query Language (OQL)。點擊圖標
輸入查詢語句
select * from INSTANCEOF MatExample
點擊執行按鈕
現在我們可以查到所有MatExample對象(本例只有一個):
點選后,在Inspector窗口中,同樣可以看到其成員變量的值。這里就不詳述了。
總結
jmap和MAT是一對非常強大的工具,通過它們可以獲知一個Java進程中所有Java級別(與Native級別相對)的數據內容。包括所有類、對象的值、引用關系、線程調用棧、局部引用,以及每個對象占用內存量等信息。而這些信息對故障追蹤可能會起到非常大的幫助。