Eclipse MAT記憶體分析工具(Memory Analyzer Tool)
MAT記憶體分析工具
MAT是Memory Analyzer的簡稱,它是一款功能強大的Java堆記憶體分析器。可以用於查詢記憶體洩露以及檢視記憶體消耗情況。MAT是基於Eclipse開發的,是一款免費的效能分析工具。讀者可以在http://www.eclipse.org/mat/下載並使用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 [email protected]>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 [email protected]>15 AND [email protected]>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(), [email protected], [email protected] FROM java.lang.String s
下例顯示了File物件的物件ID、物件地址、代理物件的型別、類的型別、物件的淺堆大小以及物件的顯示名稱。
SELECT [email protected], [email protected], [email protected], [email protected], [email protected], [email protected] FROM java.io.File f
下例顯示java.util.Vector內部陣列的長度。
SELECT [email protected] 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 ([email protected] > 2)
下例顯示物件陣列中索引下標為2的物件。
SELECT OBJECTS [email protected](2) FROM java.lang.Object[] s WHERE ([email protected] > 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 (([email protected] = 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程式效能優化》葛一鳴