Eclipse MAT官方網頁:https://www.eclipse.org/mat/downloads.php
一、MAT是什么?
MAT(Memory Analyzer Tool),一個基於Eclipse的內存分析工具,是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。使用內存分析工具從眾多的對象中進行分析,快速的計算出在內存中對象的占用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的查看到可能造成這種結果的對象。
二、如何使用
使用的方法將dump文件導入然后進行分析。
方法如下:
1、通過JMX的MBean生成當前的Heap信息,大小為一個3G(整個堆的大小)的hprof文件,如果沒有啟動JMX可以通過Java的JMAP命令來生成該文件。
2、 要考慮的是如何打開這個DUMP的堆信息文件,顯然一般的Window系統沒有這么大的內存,必須借助高配置的Linux。當然我們可以借助X-Window把Linux上的圖形導入到Window。我們考慮用下面幾種工具打開該文件:
Visual VM、IBM HeapAnalyzer、JDK 自帶的Hprof工具。
使用這些工具時為了確保加載速度,建議設置最大內存為6G。使用后發現,這些工具都無法直觀地觀察到內存泄漏,Visual VM雖能觀察到對象大小,但看不到調用堆棧;HeapAnalyzer雖然能看到調用堆棧,卻無法正確打開一個3G的文件。可以使用MAT工具直接導入文件,生成圖表信息和疑似有問題的JAVA類,如下圖所示:
三、安裝步驟
通過Eclipse市場安裝 方法/步驟
直接輸入URL安裝 方法/步驟2
四、MAT內存分析工具具體使用
1 初識MAT
在分析堆快照前,首先需要導出應用程序的堆快照。在本書前文中提到的jmap、JConsole和Visual VM等工具都可用於獲得Java應用程序的堆快照文件。此外,MAT本身也具有這個功能。
單擊左上角的“File”菜單下的“Accquire Heap Dump”選項后,會彈出當前Java應用程序列表,選擇要分析的應用程序即可,如圖所示。

除了直接在MAT中導出正在運行的應用程序堆快照外,也可以通過“Open Heap Dump”來打開一個既存的堆快照文件。
注意:使用MAT既可以打開一個已有的堆快照,也可以通過MAT直接從活動Java程序中導出堆快照。
如圖所示,顯示了正常打開堆快照文件后的MAT的界面。

右側界面中,顯示了堆快照文件的大小、類、實例和ClassLoader的總數。在右側的餅圖中,顯示了當前堆快照中最大的對象。將鼠標懸停在餅圖中,可以在左側的Inspector界面中,查看該對象的相應信息。在餅圖中單擊某對象,可以對選中的對象進行更多的操作。
在工具欄上單擊柱狀圖,可以顯示系統中所有類的內存使用情況。圖為系統內所有類的統計信息,包含類的實例數量和占用的空間。

另外一個實用的功能是,可以通過MAT查看系統中的Java線程,如圖所示。

當然,這里查看Java層面的應用線程,對於虛擬機的系統線程是無法顯示的。通過線程的堆棧,還可以查看局部變量的信息。如上圖所示,帶有“<local>”標記的,就為當前幀棧的局部變量,這部分信息可能存在缺失。
MAT的另外一個常用功能,是在各個對象的引用列表中穿梭查看。對於給定一個對象,通過MAT可以找到引用當前對象的對象,即入引用(Incomming References),以及當前對象引用的對象,即出引用(Outgoing References),如圖7.11所示。

下圖顯示了with outgoing reference 的輸出。

為了方便查看,柱狀圖還提供了根據Class Loader和包對類進行排序。如下圖是按照包排序的柱狀圖輸出。

2 淺堆和深堆
淺堆(Shallow Heap)和深堆(Retained Heap)是兩個非常重要的概念,它們分別表示一個對象結構所占用的內存大小和一個對象被GC回收后,可以真實釋放的內存大小。
淺堆(Shallow Heap)是指一個對象所消耗的內存。在32位系統中,一個對象引用會占據4個字節,一個int類型會占據4個字節,long型變量會占據8個字節,每個對象頭需要占用8個字節。
根據堆快照格式不同,對象的大小可能會向8字節進行對齊。以String對象為例,如下圖所示,顯示了String對象的幾個屬性。
- String
- value:char[]
- offset:int
- count:int
- hash:int
3個int值共占12字節,對象引用占用4字節,對象頭8字節,合計24字節。淺堆的大小只與對象的結構有關,與對象的實際內容無關。也就是說,無論字符串的長度有多少,內容是什么,淺堆的大小始終是24字節。
深堆(Retained Heap)的概念略微復雜。要理解深堆,首先需要了解保留集(Retained Set)。對象A的保留集指當對象A被垃圾回收后,可以被釋放的所有的對象集合(包括對象A本身),即對象A的保留集可以被認為是只能通過對象A被直接或間接訪問到的所有對象的集合。通俗地說,就是指僅被對象A所持有的對象的集合。深堆是指對象的保留集中所有的對象的淺堆大小之和。
注意:淺堆指對象本身占用的內存,不包括其內部引用對象的大小。一個對象的深堆指只能通過該對象訪問到的(直接或間接)所有對象的淺堆之和,即對象被回收后,可以釋放的真實空間。
另外一個常用的概念是對象的實際大小。這里,對象的實際大小定義為一個對象所能觸及的所有對象的淺堆大小之和,也就是通常意義上我們說的對象大小。與深堆相比,似乎這個在日常開發中更為直觀和被人接受,但實際上,這個概念和垃圾回收無關。
如圖7.14所示,顯示了一個簡單的對象引用關系圖,對象A引用了C和D,對象B引用了C和E。那么對象A的淺堆大小只是A本身,不含C和D,而A的實際大小為A、C、D三者之和。而A的深堆大小為A與D之和,由於對象C還可以通過對象B訪問到,因此不在對象A的深堆范圍內。

在MAT中查看對象淺堆和深堆的大小:

選中對象,單擊右鍵,在彈出的菜單中都有 Show Retained Set 命令,它可用於顯示指定類或者對象的保留集。
3 支配樹(Dominator Tree)
MAT提供了一個稱為支配樹(Dominator Tree)的對象圖。支配樹體現了對象實例間的支配關系。在對象引用圖中,所有指向對象B的路徑都經過對象A,則認為對象A支配對象B。如果對象A是離對象B最近的一個支配對象,則認為對象A為對象B的直接支配者。支配樹是基於對象間的引用圖所建立的,它有以下基本性質:
- 對象A的子樹(所有被對象A支配的對象集合)表示對象A的保留集(retained set),即深堆。
- 如果對象A支配對象B,那么對象A的直接支配者也支配對象B。
- 支配樹的邊與對象引用圖的邊不直接對應。
如圖7.19所示,左圖表示對象引用圖,右圖表示左圖所對應的支配樹。對象A和B由根對象直接支配,由於在到對象C的路徑中,可以經過A,也可以經過B,因此對象C的直接支配者也是根對象。對象F與對象D相互引用,因為到對象F的所有路徑必然經過對象D,因此,對象D是對象F的直接支配者。而到對象D的所有路徑中,必然經過對象C,即使是從對象F到對象D的引用,從根節點出發,也是經過對象C的,所以,對象D的直接支配者為對象C。

同理,對象E支配對象G。到達對象H的可以通過對象D,也可以通過對象E,因此對象D和E都不能支配對象H,而經過對象C既可以到達D也可以到達E,因此對象C為對象H的直接支配者。
在MAT中,單擊工具欄上的對象支配樹按鈕,可以打開對象支配樹視圖,如圖7.20所示。

注意:對象支配樹中,某一個對象的子樹,表示在該對象被回收后,也將被回收的對象的集合。
4 垃圾回收根
在Java系統中,作為垃圾回收的根節點可能是以下對象之一:
- 系統類:被 bootstrap/system ClassLoader加載的類。如在 rt.jar包中的所有類。
- JNI局部變量:本地代碼中的局部變量。如用戶自定義的JNI代碼或者JVM內部代碼。
- JNI全局變量:本地代碼中的全局變量。
- 線程:開始、並且沒有停止的線程。
- 正在使用的鎖:作為鎖的對象。比如,調用了 wait() 或者 notify() 方法的對象。或者調用了 synchronized(Object)操作的對象。
- Java局部變量:如函數的輸入參數以及方法中的局部變量。
- 本地棧:本地代碼中的輸入輸出參數。比如用戶自定義的JNI代碼或者JVM內部代碼。
- Finalizer:在等待隊列中將要被執行析構函數的對象。
- Unfinalized:擁有析構函數,但是沒有被析構,且不在析構隊列中的對象。
- 不可達對象:從任何一個根對象,都無法達到的對象。但為了能夠在MAT中分析,被MAT標志位根。
- 未知對象:未知的根類型。用於處理一些特殊的堆格式。
通過MAT,可以列出所有的根對象,如下圖所示。

5 內存泄漏檢測
MAT 提供了自動檢測內存泄漏,以及統計堆快照內對象分布情況的工具,如圖所示:

6 最大對象報告
系統中占有內存最大的幾個對象,往往是解決系統性能問題的關鍵所在。如果應用程序發生內存泄漏,那么泄漏的對象通常會在堆快照中所占據很大的比重。因此,查看和分析堆快照中最大的對象,具有較高的價值。
在MAT中,可以自動查找並顯示消耗內存最多的幾個對象,如圖所示,可以打開以餅圖和表格為形式的最大對象報告。

7 查找支配者
通過MAT,開發人員還可以很方便地查找某一個對象或者類的支配者。如下圖所示。

在參數對話框中,務必正確填寫 -skip 參數。查詢結果會忽略所有定義在 -skip 參數中的類和實例。
輸出結果是不滿足 -skip 所指定正則表達式的、所有選中對象或類的直接的支配者。
8 線程分析

9 集合使用情況分析

使用這些工具,可以查看數組、集合的填充率;可以觀察集合內的數據;也可以分析哈希表的沖突率。
MAT對OQL的支持
MAT的OQL語法與Visual VM支持的OQL有着很大不同。MAT支持一種類似於SQL的查詢語言OQL(Object Query Language)。OQL使用類SQL語法,可以在堆中進行對象的查找和篩選。本節將主要介紹OQL的基本使用方法,幫助讀者盡快掌握這種堆文件的查看方式。
1 Select子句
在MAT中,Select子句的格式與SQL基本一致,用於指定要顯示的列。Select子句中可以使用“*”,查看結果對象的引用實例(相當於outgoing references)。
select * from java.util.ArrayList A
以上查詢的輸出如圖所示,在輸出結果中,結果集中的每條記錄都可以展開,查看各自的引用對象。

OQL還可以指定對象的屬性進行輸出,下例輸出所有Vector對象的內部數組,輸出結果如圖7.31所示。使用“OBJECTS”關鍵字,可以將返回結果集中的項以對象的形式顯示。
SELECT OBJECTS v.elementData FROM java.util.Vector v

下例顯示String對象的char數組(用於JDK 1.7的堆):
SELECT OBJECTS s.value FROM java.lang.String s
在Select子句中,使用“AS RETAINED SET”關鍵字可以得到所得對象的保留集。下例得到geym.zbase.ch7.heap.Student對象的保留集。
SELECT AS RETAINED SET * FROM geym.zbase.ch7.heap.Student
“DISTINCT”關鍵字用於在結果集中去除重復對象。下例的輸出結果中只有一條“class java.lang.String”記錄。如果沒有“DISTINCT”,那么查詢將為每個String實例輸出其對應的Class信息。
SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s
2 From子句
From子句用於指定查詢范圍,它可以指定類名、正則表達式或者對象地址。
下例使用From子句,指定類名進行搜索,並輸出所有的java.lang.String實例。
SELECT * FROM java.lang.String s
下例使用正則表達式,限定搜索范圍,輸出所有java.lang包下所有類的實例,如圖所示。
SELECT * FROM "java\.lang\..*"

也可以直接使用類的地址進行搜索。使用類的地址的好處是可以區分被不同ClassLoader加載的同一種類型。下例中“0x37a014d8”為類的地址。
select * from 0x37a014d8
有多種方法可以獲得類的地址,在MAT中,一種最為簡單的方法如圖所示。

在From子句中,還可以使用“INSTANCEOF”關鍵字,返回指定類的所有子類實例。下例的查詢返回了當前堆快照中所有的抽象集合實例,包括java.util.Vector、java.util.ArrayList和java.util.HashSet等。
SELECT * FROM INSTANCEOF java.util.AbstractCollection
在From子句中,還可以使用“OBJECTS”關鍵字。使用“OBJECTS”關鍵字后,那么原本應該返回類的實例的查詢,將返回類的信息。
SELECT * FROM OBJECTS java.lang.String
以上查詢的返回結果如圖所示。它僅返回一條記錄,表示java.lang.String的類的信息。如果不使用“OBJECTS”關鍵字,這個查詢將返回所有的java.lang.String實例。

“OBJECTS”關鍵字也支持與正則表達式一起使用。下面的查詢,返回了所有滿足給定正則表達式的所有類,其結果如圖所示。
SELECT * FROM OBJECTS "cn\.zyzpp\..*"

注意:在From子句中使用OBJECTS關鍵字,將返回符合條件的類信息,而非實例信息。這與Select子句中的OBJECTS關鍵字是完全不同的。
3 Where子句
Where子句用於指定OQL的查詢條件。OQL查詢將只返回滿足Where子句指定條件的對象。Where子句的格式與傳統SQL極為相似。
下例返回長度大於10的char數組。
SELECT * FROM char[] s WHERE s.@length>10
下例返回包含“java”子字符串的所有字符串,使用“LIKE”操作符,“LIKE”操作符的操作參數為正則表達式。
SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*java.*"
下例返回所有value域不為null的字符串,使用“=”操作符。
SELECT * FROM java.lang.String s where s.value!=null
Where子句支持多個條件的AND、OR運算。下例返回數組長度大於15,並且深堆大於1000字節的所有Vector對象。
SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapSize>1000
4 內置對象與方法
OQL中可以訪問堆內對象的屬性,也可以訪問堆內代理對象的屬性。訪問堆內對象的屬性時,格式如下:
[ <alias>. ] <field> . <field>. <field>
其中alias為對象名稱。
下例訪問java.io.File對象的path屬性,並進一步訪問path的value屬性。
SELECT toString(f.path.value) FROM java.io.File f
以上查詢得到的結果如圖7.38所示。

這些堆內對象的屬性與Java對象一致,擁有與Java對象相同的結果。
MAT為了能快捷地獲取堆內對象的額外屬性(比如對象占用的堆大小、對象地址等),為每種元類型的堆內對象建立了相對應的代理對象,以增強原有的對象功能。訪問代理對象的屬性時,使用如下格式:
[ <alias>. ] @<attribute>
其中,alias為對象名稱,attribute為屬性名。
下例顯示了String對象的內容、objectid和objectAddress。
SELECT s.toString(), s.@objectId, s.@objectAddress FROM java.lang.String s
下例顯示了File對象的對象ID、對象地址、代理對象的類型、類的類型、對象的淺堆大小以及對象的顯示名稱。
SELECT f.@objectId, f.@objectAddress, f.@class, f.@clazz, f.@usedHeapSize, f.@displayName FROM java.io.File f
下例顯示java.util.Vector內部數組的長度。
SELECT v.elementData.@length FROM java.util.Vector v
下表整理了MAT代理對象的基本屬性。
對象類型 |
接口 |
屬性 |
功能 |
---|---|---|---|
基對象 |
IObejct |
objectId |
對象ID |
objectAddress |
對象地址 |
||
class |
代理對象類型 |
||
clazz |
對象類類型 |
||
usedHeapSize |
淺堆大小 |
||
retainedHeapSize |
深堆大小 |
||
displayName |
顯示名稱 |
||
Class對象 |
IClass |
classLoaderId |
ClassLoad的ID |
數組 |
IArray |
length |
數組長度 |
元類型數組 |
IPrimitiveArray |
valueArray |
數組內容 |
對象數組 |
IObjectArray |
referenceArray |
數組內容 |
除了使用代理對象的屬性,OQL中還可以使用代理對象的方法,使用格式如下:
[ <alias> . ] @<method>( [ <expression>, <expression> ] )
下例顯示int數組中索引下標為2的數據內容。
SELECT s.getValueAt(2) FROM int[] s WHERE (s.@length > 2)
下例顯示對象數組中索引下標為2的對象。
SELECT OBJECTS s.@referenceArray.get(2) FROM java.lang.Object[] s WHERE (s.@length > 2)
下例顯示了當前堆中所有的類型。
select * from ${snapshot}.getClasses()
下例顯示了所有的java.util.Vector對象及其子類型,它的輸出如圖所示。
select * from INSTANCEOF java.util.Vector

下例顯示當前對象是否是數組。
SELECT c, classof(c).isArrayType() FROM ${snapshot}.getClasses() c
代理對象的方法整理如表所示。
表 MAT代理對象的方法
對象說明 |
對象名 |
對象方法 |
對象方法說明 |
---|---|---|---|
全局快照 |
ISnapshot |
getClasses() |
所有實例的集合 |
getClassesByName(String name, boolean includeSubClasses) |
根據名稱選取符合條件的實例 |
||
類對象 |
IClass |
hasSuperClass() |
是否有超類 |
isArrayType() |
是否是數組 |
||
基對象 |
IObject |
getObjectAddress() |
取得對象地址 |
元類型數組 |
IPrimitiveArray |
getValueAt(int index) |
取得數組中給定索引的數據 |
元類型數組,對象數組 |
[] or List |
get(int index) |
取得數組中給定索引的數據 |
MAT的OQL中還內置一些有用的函數,如表所示。
表 OQL中的內置函數
函數 |
說明 |
---|---|
toHex( number ) |
轉為16進制 |
toString( object ) |
轉為字符串 |
dominators( object ) |
取得直接支配對象 |
outbounds( object ) |
取得給定對象引用的對象 |
inbounds( object ) |
取得引用給定對象的對象 |
續表
函數 |
說明 |
---|---|
classof( object ) |
取得當前對象的類 |
dominatorof( object ) |
取得給定對象的直接支配者 |
下例顯示所有長度為15的字符串內容(JDK 1.7導出的堆)。
SELECT toString(s) FROM java.lang.String s WHERE ((s.value.@length = 15) and (s.value != null))
下例顯示所有cn.zyzpp.jConsole.HProfTest對象的直接支配對象。即給定對象回收后,將釋放的對象集合。
SELECT objects dominators(s) FROM cn.zyzpp.jConsole.HProfTest s
以上查詢的輸出如圖所示,顯示HProfTest對象支配了1個字符串對象。

函數dominatorof()與dominators()的功能相反,它獲取直接支配當前對象的對象。
SELECT distinct objects dominatorof(s) FROM cn.zyzpp.jConsole.HProfTest s
以上查詢的輸出如圖所示,顯示所有的HProfTest對象直接被主線程支配。

注意:函數dominatorof()與dominators()的功能正好相反。dominatorof()用於獲得直接支配當前對象的對象,而dominators()用於獲取直接支配對象。
下例取得引用WebPage的對象。
SELECT objects inbounds(w) FROM geym.zbase.ch7.heap.WebPage w
下例取得堆快照中所有在cn.zyzpp包中的存在對象實例的類型,其輸出如圖所示。
SELECT distinct objects classof(obj) FROM "cn\.zyzpp\..*" obj

參考
《Java程序性能優化》葛一鳴