MAT內(nèi)存分析工具
MAT是MemoryAnalyzerTool的簡稱,它是一款功能強(qiáng)大的Java堆內(nèi)存分析器,可以用于查找內(nèi)存泄漏以及查看內(nèi)存消耗情況。MAT是
基于Eclipse開發(fā)的一款免費(fèi)的性能分析工具,讀者可以在
http://www.eclipse.org/mat/上下載并使用MAT。
一,初識(shí)MAT
在分析堆快照前,首先需要導(dǎo)出應(yīng)用程序的堆快照。在本書前文中提到的jmap、JConsole和VisualVM等工具都可以用于獲得Java應(yīng)用程序的堆快照文件。此外,MAT本身也具有這個(gè)功能。
如圖6.66所示,在File菜單中選擇AcquireHeapDump命令,在彈出對(duì)話框的當(dāng)前Java應(yīng)用程序列表中選擇要分析的應(yīng)用程序即可,如圖6.67所示。


除了直接在MAT中導(dǎo)出正在運(yùn)行的應(yīng)用程序堆快照外,也可以通過File菜單中的OpenHeapDump命令打開一個(gè)既存的堆快照文件。
注意:使用MAT既可以打開一個(gè)已有的堆快照,也可以直接從活動(dòng)Java程序中導(dǎo)出堆快照。
圖6.68所示為正常打開堆快照文件后的MAT界面。

在圖6.68的右側(cè)界面中顯示了堆快照文件的大小、類、實(shí)例和ClassLoader的總數(shù);餅圖中顯示了當(dāng)前堆快照中最大的對(duì)象。將光標(biāo)懸停在餅圖中,可以在左側(cè)的Inspector界面中查看該對(duì)象的詳細(xì)信息。在餅圖中單擊,可以對(duì)選中的對(duì)象進(jìn)行更多的操作。
單擊工具欄上的柱狀圖按鈕(如圖6.69所示),可以查看當(dāng)前堆的類信息,包括類的對(duì)象數(shù)量、淺堆(Shallow)大小和深堆(Retained)大小,如圖6.70所示。

通過柱狀圖界面,可以查找引用選中對(duì)象的對(duì)象集合以及選中對(duì)象所引用的對(duì)象集合。如圖6.71所示,選中java.util.Vector對(duì)象并右擊,在彈出的右鍵菜單中選擇Listobjects命令,彈出的withoutgoingreferences和withincomingreferences子命令分別表示查找java.util.Vector實(shí)例的引用對(duì)象,以及引用java.util.Vector實(shí)例的對(duì)象。

注意:通過MAT,可以根據(jù)對(duì)象間的引用關(guān)系對(duì)內(nèi)存中的對(duì)象進(jìn)行分析。
圖6.72顯示了選擇withincomingreferences命令后的輸出結(jié)果,展示了兩個(gè)被主線程引用的java.util.Vector局部變量實(shí)例。
為了方便查看,柱狀圖還可以根據(jù)ClassLoader和包對(duì)類進(jìn)行排序。圖6.73顯示了MAT的柱狀圖排序功能,以及一個(gè)按照包進(jìn)行排序的柱狀圖輸出命令。

圖6.72 引用關(guān)系查詢結(jié)果

二,淺堆和深堆
淺堆(ShallowHeap)和深堆(RetainedHeap)是兩個(gè)非常重要的概念,它們分別表示一個(gè)對(duì)象結(jié)構(gòu)所占用的內(nèi)存大小和一個(gè)對(duì)象被執(zhí)行GC操作后,可以真實(shí)釋放的內(nèi)存大小。
淺堆是指一個(gè)對(duì)象所消耗的內(nèi)存。在32位系統(tǒng)中,一個(gè)對(duì)象引用會(huì)占據(jù)4個(gè)字節(jié),一個(gè)int類型會(huì)占據(jù)4個(gè)字節(jié),long型變量會(huì)占據(jù)8個(gè)字節(jié),每個(gè)對(duì)象頭需要占用8個(gè)字節(jié)。
根據(jù)堆快照格式不同,對(duì)象的大小可能會(huì)向8字節(jié)進(jìn)行對(duì)齊。以String對(duì)象為例,圖6.74顯示了String對(duì)象的幾個(gè)屬性。
3個(gè)int類型以及一個(gè)引用類型合計(jì)占用的內(nèi)存為3×4+4=16字節(jié),再加上對(duì)象頭的8個(gè)字節(jié),因此String對(duì)象占用的空間,即淺堆的大小是16+8=24字節(jié)。淺堆的大小只與對(duì)象的結(jié)構(gòu)有關(guān),與對(duì)象的實(shí)際內(nèi)容無關(guān)。也就是說,無論字符串的長度是多少,內(nèi)容是什么,淺堆的大小始終是24字節(jié)。

深堆的概念略微復(fù)雜。要理解深堆,首先需要了解保留集(RetainedSet)。對(duì)象A的保留集指當(dāng)對(duì)象A被垃圾回收后,可以被釋放的所有的對(duì)象集合(包括對(duì)象A本身),即對(duì)象A的保留集可以被認(rèn)為是只能通過對(duì)象A被直接或者間接訪問到的所有對(duì)象的集合。通俗地說,就是指僅被對(duì)象A所持有的對(duì)象的集合。深堆是指對(duì)象的保留集中所有對(duì)象的淺堆之和。
注意:淺堆指對(duì)象本身占用的內(nèi)存,不包括其內(nèi)部引用對(duì)象的大小。一個(gè)對(duì)象的深堆指只能通過該對(duì)象訪問到的(直接或間接)所有對(duì)象的淺堆之和,即對(duì)象被回收后,可以釋放的真實(shí)空間。
下面這個(gè)例子很好地詮釋了深堆的概念。首先是表示點(diǎn)的類定義:

接著是表示線的類定義:

主函數(shù)構(gòu)造了a、b、c、d、e、f、g這7個(gè)點(diǎn),以及aLine、bLine、cLine和dLine這4條線,并在程序最后將a、b、c、d、e這5個(gè)點(diǎn)設(shè)置為null。具體代碼如下:

這段代碼的對(duì)象引用關(guān)系如圖6.75所示,其中a、b、c、d、e對(duì)象在使用完成后被設(shè)置為null。

根據(jù)Point類的結(jié)構(gòu),一個(gè)Point實(shí)例的淺堆大小為4×2+8=16字節(jié),一個(gè)Line實(shí)例的淺堆大小為4×2+8=16字節(jié)。使用MAT得到該示例的內(nèi)存快照文件,如圖6.76所示。為了閱讀方便,筆者將代碼中的變量名標(biāo)識(shí)到了內(nèi)存快照中的對(duì)象上。

可以看到,所有的Point實(shí)例淺堆和深堆的大小都是16字節(jié)。而dLine對(duì)象,淺堆為16字節(jié),深堆也是16字節(jié),這是因?yàn)閐Line對(duì)象內(nèi)的兩個(gè)點(diǎn)f和g沒有被設(shè)置為null,因此即使dLine被回收,f和g也不會(huì)被釋放。對(duì)象cLine內(nèi)的引用對(duì)象d和e由于僅在cLine內(nèi)還存在引用,因此只要cLine被釋放,d和e必然也作為垃圾被回收,即d和e在cLine的保留集內(nèi),因此cLine的深堆為16×2+16=48字節(jié)。
對(duì)于aLine和bLine對(duì)象,由于兩者均持有對(duì)方的一個(gè)點(diǎn),因此當(dāng)aLine被回收時(shí),公共點(diǎn)a在bLine中依然有引用存在,故不會(huì)被回收,點(diǎn)a不在aLine對(duì)象的保留集中,因此aLine的深堆大小為16+16=32字節(jié)。對(duì)象bLine與aLine完全一致。
在MAT中,無論是在柱狀圖還是對(duì)象列表中,選中對(duì)象并右擊,在彈出的快捷菜單中都有ShowRetainedSet命令,它可用于顯示指定類或者對(duì)象的保留集。圖6.77和圖6.78分別為在bLine對(duì)象上進(jìn)行該操作,以及bLine對(duì)象的保留集。


三,支配樹
MAT提供了一個(gè)稱為支配樹(DominatorTree)的對(duì)象圖。支配樹體現(xiàn)了對(duì)象實(shí)例間的支配關(guān)系。在對(duì)象引用圖中,所有指向?qū)ο驜的路徑都經(jīng)過對(duì)象A,則認(rèn)為對(duì)象A支配對(duì)象B。如果對(duì)象A是離對(duì)象B最近的一個(gè)支配對(duì)象,則認(rèn)為對(duì)象A為對(duì)象B的直接支配者。支配樹是基于對(duì)象間的引用圖所建立的,它具有以下基本性質(zhì):
·對(duì)象A的子樹(所有被對(duì)象A支配的對(duì)象集合)表示對(duì)象A的保留集(retainedset)。
·如果對(duì)象A支配對(duì)象B,那么對(duì)象A的直接支配者也支配對(duì)象B。
·支配樹的邊與對(duì)象引用圖的邊不直接對(duì)應(yīng)。
如圖6.79所示,左圖表示對(duì)象引用圖,右圖表示左圖所對(duì)應(yīng)的支配樹。對(duì)象A和B由根對(duì)象直接支配,由于在到對(duì)象C的路徑中可以經(jīng)過A,也可以經(jīng)過B,因此對(duì)象C的直接支配者也是根對(duì)象。對(duì)象F與對(duì)象D相互引用,因?yàn)榈綄?duì)象F的所有路徑必然經(jīng)過對(duì)象D,因此對(duì)象D是對(duì)象F的直接支配者。而到對(duì)象D的所有路徑中,必然經(jīng)過對(duì)象C,即使是從對(duì)象F到對(duì)象D的引用,從根節(jié)點(diǎn)出發(fā),也是經(jīng)過對(duì)象C的,所以對(duì)象D的直接支配者為對(duì)象C。

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

圖6.81顯示了對(duì)象支配樹視圖的一部分。該截圖顯示部分main線程對(duì)象的直接支配對(duì)象,即main線程對(duì)象被回收后將被釋放的所有對(duì)象的集合。
注意:在對(duì)象支配樹中,某一個(gè)對(duì)象的子樹表示在該對(duì)象被回收后也將被回收的對(duì)象的集合。

四,垃圾回收根
在Java系統(tǒng)中,作為垃圾回收的根節(jié)點(diǎn)可能是以下對(duì)象之一。
·系統(tǒng)類:被
bootstrap/systemClassLoader加載的類,例如在rt.jar包中的所有類。
·JNI局部變量:本地代碼中的局部變量,例如用戶自定義的JNI代碼或者JVM內(nèi)部代碼。
·JNI全局變量:本地代碼中的全局變量。
·線程:開始,并且沒有停止的線程。
·在用同步鎖:作為鎖的對(duì)象。例如調(diào)用了wait()或者notify()方法的對(duì)象,或者調(diào)用了synchronized(Object)操作的對(duì)象。
·Java局部變量:如函數(shù)的輸入?yún)?shù)及方法中的局部變量。
·本地棧:本地代碼中的輸入、輸出參數(shù),例如用戶自定義的JNI代碼或者JVM內(nèi)部代碼。
·Finalizer:在等待隊(duì)列中將要被執(zhí)行析構(gòu)函數(shù)的對(duì)象。
·Unfinalized:擁有析構(gòu)函數(shù),但是沒有被析構(gòu)且不在析構(gòu)隊(duì)列中的對(duì)象。
·不可達(dá)對(duì)象:從任何一個(gè)根對(duì)象都無法到達(dá)的對(duì)象。但為了能夠在MAT中分析,被MAT標(biāo)志為根。
·未知對(duì)象:未知的根類型,用于處理一些特殊的堆格式。
通過MAT,可以列出所有的根對(duì)象,如圖6.82所示。

五,內(nèi)存泄漏檢測
MAT提供了自動(dòng)檢測內(nèi)存泄漏,以及統(tǒng)計(jì)堆快照內(nèi)對(duì)象分布情況的工具。圖6.83展示了內(nèi)存泄漏檢測工具的使用方法。選擇菜單中的LeakSuspects命令,MAT會(huì)自動(dòng)生成一份報(bào)告。這份報(bào)告羅列了系統(tǒng)內(nèi)可能存在內(nèi)存泄漏的問題點(diǎn)。圖6.84展示了報(bào)告中給出的一個(gè)問題點(diǎn)樣例。

注意:仔細(xì)閱讀MAT給出的內(nèi)存泄漏報(bào)告,可以幫助開發(fā)人員更快地找到系統(tǒng)的潛在問題。

六,最大對(duì)象報(bào)告
系統(tǒng)中占用內(nèi)存最大的幾個(gè)對(duì)象,往往是解決系統(tǒng)性能問題的關(guān)鍵所在。如果應(yīng)用程序發(fā)生內(nèi)存泄漏,那么泄漏的對(duì)象通常會(huì)在堆快照中占據(jù)很大的比重。因此,查看和分析堆快照中最大的對(duì)象具有較高的價(jià)值。
在MAT中,可以自動(dòng)查找并顯示消耗內(nèi)存最多的幾個(gè)對(duì)象。如圖6.85所示,通過選擇TopConsumers命令,可以打開消耗內(nèi)存最多的對(duì)象的報(bào)告,其中主要以餅圖和表格的形式來展示。

七,查找支配者
通過MAT,開發(fā)人員還可以很方便地查找某一個(gè)對(duì)象或者類的支配者(有關(guān)支配者的概念,可以參考6.7.3節(jié)“支配樹”)。雖然在支配樹頁面中擁有完整的信息,但是通過MAT提供的支配者查找功能可以更方便地進(jìn)行查找。圖6.86顯示了如何查找對(duì)象的支配者。

在選擇ImmediateDominators命令后,會(huì)彈出一個(gè)參數(shù)對(duì)話框,用于設(shè)置查找參數(shù),如圖6.87所示。在參數(shù)對(duì)話框中,注意務(wù)必正確輸入-skip參數(shù),否則查詢結(jié)果會(huì)忽略所有定義在-skip參數(shù)中的類和實(shí)例。
ImmediateDominators會(huì)輸出選中對(duì)象的直接支配者(將-skip指定的對(duì)象排除在外)。

八,線程分析
在堆快照中,還包括當(dāng)前的線程信息,通過MAT可以查看這些信息。如圖6.88所示,通過ThreadDetails、ThreadOverview和ThreadStacks這3個(gè)命令,可以查看線程詳情。

圖6.89所示為選擇ThreadStacks命令后的輸出結(jié)果,其中顯示了當(dāng)前堆快照中的所有線程及線程引用的對(duì)象。

九,集合使用情況分析
MAT提供了一套對(duì)集合使用狀態(tài)進(jìn)行分析的工具,如圖6.90所示。

使用這些工具,可以查看數(shù)組、集合的填充率;可以觀察集合內(nèi)的數(shù)據(jù);也可以分析哈希表的沖突率。
注意:通過對(duì)集合使用情況進(jìn)行分析,可以更好地了解系統(tǒng)的內(nèi)存使用情況,查找浪費(fèi)的內(nèi)存空間。
選擇CollectionFillRatio命令,可以展示給定集合的填充率。圖6.91所示為該功能的輸出結(jié)果,其中顯示了填充率為0、20%以下、80%以下和100%以下的集合個(gè)數(shù)。

通過選擇HashEntries命令,可以查看Hash表的內(nèi)容。圖6.92所示為該功能的一個(gè)輸出示例,其中顯示了選中的Hash表的內(nèi)容。對(duì)于表中的Key和Value對(duì)象,通過右鍵快捷菜單,還可以進(jìn)一步分析它們的引用情況和其他具體信息。

十,擴(kuò)展MAT
MAT是基于Eclipse開發(fā)平臺(tái)的產(chǎn)品,因此它也具有很好的擴(kuò)展性。開發(fā)者可以使用Eclipse對(duì)MAT進(jìn)行擴(kuò)展,從而實(shí)現(xiàn)符合開發(fā)人員需要的功能更加強(qiáng)勁的內(nèi)存分析工具。通過擴(kuò)展MAT,讀者可以實(shí)現(xiàn)諸如自動(dòng)對(duì)象查詢、優(yōu)化界面顯示、報(bào)表增強(qiáng)等功能。本節(jié)將通過一個(gè)簡單的MAT插件,介紹擴(kuò)展MAT的基本步驟和方法。
注意:MAT是基于Eclipse的,因此對(duì)MAT進(jìn)行二次開發(fā)與開發(fā)Eclipse插件非常類似。
在Java中,java.lang.String對(duì)象實(shí)現(xiàn)是基于內(nèi)部的value字符數(shù)組、偏移量offset和字符串長度count來定義字符串String的真實(shí)取值的。如果內(nèi)部數(shù)組value的實(shí)際長度很長,而字符串真實(shí)長度count的數(shù)值很小,則說明這個(gè)String的內(nèi)存使用率不高,存在較為嚴(yán)重的內(nèi)存浪費(fèi)。
使用公式count/value.length可以計(jì)算當(dāng)前String對(duì)象的內(nèi)存使用率。在最優(yōu)情況下,String對(duì)象的內(nèi)存使用率是100%,即表示value數(shù)組中的所有字符都是當(dāng)前字符串的內(nèi)容。當(dāng)使用類似String.subString()的函數(shù)生成新的字符串時(shí),String對(duì)象通過調(diào)整offset和count,而非創(chuàng)建新的value數(shù)組來生成新的字符串,此時(shí)String對(duì)象的內(nèi)存利用率就會(huì)下降。
本節(jié)中展示的插件將在顯示String對(duì)象時(shí),展示String對(duì)象的內(nèi)存利用率,幫助開發(fā)者快速定位可以優(yōu)化的字符串對(duì)象。
為擴(kuò)展MAT,首先需要安裝MAT程序及Eclipse開發(fā)工具。
(1)在Eclipse平臺(tái)中添加MAT目標(biāo)平臺(tái)。在Eclipse中打開對(duì)話框:Windows|Preferences|Plug-inDevelopment|TargetPlatform。添加MAT平臺(tái),選擇Add|Nothing|Next。在目標(biāo)平臺(tái)的Locations頁面中,添加Installation,并指定MAT的安裝路徑,如圖6.93所示。單擊Finish按鈕,并選擇剛剛添加的MAT平臺(tái)作為目標(biāo)平臺(tái)。圖6.94所示為配置完成后的目標(biāo)平臺(tái)。

(2)創(chuàng)建一個(gè)插件工程。選擇File|New|Other|Plug-inproject命令,假設(shè)工程名稱是MATExtension,其他參數(shù)可以使用默認(rèn)設(shè)置。創(chuàng)建完成后,在工程的Dependencies頁面中添加org.eclipse.mat.api依賴,如圖6.95所示。


(3)添加插件的擴(kuò)展點(diǎn)。在本例中添加
org.eclipse.mat.api.nameResolver,如圖6.96所示。在實(shí)際開發(fā)中,讀者可以根據(jù)自己的需要,選擇合適的擴(kuò)展點(diǎn)增強(qiáng)MAT的功能。接著填寫擴(kuò)展點(diǎn)的具體信息,如實(shí)現(xiàn)擴(kuò)展點(diǎn)接口的類名和包名,Eclipse會(huì)自動(dòng)生成指定的類,如圖6.97所示。


編輯生成的StringUsageDisplayer類,具體代碼如下:

StringUsageDisplayer的功能是當(dāng)MAT中顯示String對(duì)象時(shí),計(jì)算String對(duì)象的count值與value數(shù)組的長度比值。注釋@Subject指定當(dāng)前
IClassSpecificNameResolver只對(duì)java.lang.String對(duì)象有效。
(4)當(dāng)完成開發(fā)后,還需要對(duì)插件進(jìn)行打包。選擇File|Export|Plug-inDevelopment|
Deployableplug-insandfragments命令,在打開的對(duì)話框中選中要打包的插件,并設(shè)置MAT的安裝路徑進(jìn)行插件安裝,如圖6.98所示。
安裝完成后,在MAT的plugins目錄下就有了MATExtension插件的JAR包。
安裝插件后的MAT,可以使用以下OQL查詢?nèi)〉盟袃?nèi)存利用率不是100%的String。

查詢結(jié)果如圖6.99所示,其中不僅顯示了字符串的真實(shí)取值,也顯示了當(dāng)前字符串的內(nèi)存使用率,可以幫助開發(fā)人員快速定位能夠優(yōu)化的字符串。
注意:通過對(duì)MAT的擴(kuò)展,可以讓MAT更貼近實(shí)際生產(chǎn)環(huán)境,使之更易于使用,提高了堆內(nèi)存分析的效率。

