本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。
MAT是分析Java堆內存的一個工具,全稱是 The Eclipse Memory Analyzer Tool,用來幫助分析內存泄漏和減少內存消耗。使用MAT分析Java堆快照,可以快速計算出對象的保留大小(Retained Sizes),查找到阻止對象被回收的原因,MAT會自動生成一個包含內存泄漏疑點的報告。
MAT可以從Eclipse網站下載:http://www.eclipse.org/mat/
生成Dump
使用MAT分析的是Heap Dump,也就是堆內存快照,生成快照有以下幾種方式:
- 使用虛擬機參數-XX:+HeapDumpOnOutOfMemoryError,溢出時自動生成快照。
- 使用jmap命令,jmap -dump:format=b,file=${dir}/jmap.hprof pid
- 使用MAT導出本地java進程的內存快照,File->Acquire Heap Dump->選擇要dump的java進程就可以了。
MAT的使用
生成完dump之后,可以用MAT打開dump出來的快照文件,File -> Open Heap Dump,對dump文件進行分析,生成一個Overview視圖:

首先會列出堆內存的大小,有多少個類,有多少個對象,以及多少個類加載器。
然后是一個根據對象的Retained Size大小形成的餅狀圖,鼠標放上去,左側Inspector視圖會顯示這個對象的詳細信息。
然后是其他功能,比如常用的圖表,Histogram直方圖、Dominator Tree支配樹,還會生成一個分析報告Leak Suspects Report。
基礎概念
繼續分析之前,先了解幾個基礎概念。
Shallow Heap 和 Retained Heap
Shallow Heap表示對象本身占用內存的大小,不包含對其他對象的引用,也就是對象頭加成員變量(不是成員變量的值)的總和。
Retained Heap是該對象自己的Shallow Heap,並加上從該對象能直接或間接訪問到對象的Shallow Heap之和。換句話說,Retained Heap是該對象GC之后所能回收到內存的總和。
把內存中的對象看成下圖中的節點,並且對象和對象之間互相引用。這里有一個特殊的節點GC Roots,這就是reference chain的起點。

從obj1入手,上圖中藍色節點代表僅僅只有通過obj1才能直接或間接訪問的對象。因為可以通過GC Roots訪問,所以左圖的obj3不是藍色節點;而在右圖卻是藍色,因為它已經被包含在retained集合內。所以對於左圖,obj1的retained size是obj1、obj2、obj4的shallow size總和;右圖的retained size是obj1、obj2、obj3、obj4的shallow size總和。obj2的retained size可以通過相同的方式計算。
對象引用 Reference
關於對象的引用,前面的文章講到過,划分如下:
-
強引用(Strong Reference)就是在代碼中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收被引用的對象。
-
軟引用(Soft Reference)是用來描述有用非必需的對象。軟引用關聯的對象,在系統將要發生內存溢出之前,將會對這些對象進行二次回收。如果這次回收后還沒有足夠的內存,才會拋出內存溢出異常。上面所說的“食之無味,棄之可惜”的對象就是屬於軟引用。
-
弱引用(Weak Reference)是用來描述非必需的對象,但是比軟引用更弱一些,弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當下一次垃圾收集時,無論內存是否足夠,都會回收掉被弱引用關聯的對象。
-
虛引用(Phantom Reference)也稱為幽靈引用或者幻影引用,它是最弱的一種引用。一個對象是否有虛引用存在,完全不會對其生存時間造成任何影響,也無法通過虛引用獲得一個對象實例。為對象設置虛引用的目的,就是能在這個對象被收集器回收時收到一個系統通知。
四種引用中,只有強引用是強可達性,根據可達性分析回收內存時,永遠不會被回收。
GC Roots 和 引用鏈
JVM在進行GC的時候是通過使用可達性來判斷對象是否存活,通過GC Roots(GC根節點)的對象作為起始點,從這些節點開始進行向下搜索,搜索所走過的路徑成為Reference Chain(引用鏈),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
如下圖所示,對象Object 5、Object 6、Object 7雖然互相關聯,但是它們到GC Roots是不可達的,所以它們將被判定為可回收的對象:

在 Java 中,可作為 GC Roots 的對象有以下幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。
四種引用,GC Roots以及引用鏈,可以參考之前的博客文章:《JVM探秘:四種引用、對象的生存與死亡》
Histogram 直方圖
點擊工具欄上的
圖標,打開 Histogram 直方圖視圖,可以列出每個類產生的實例數量,以及所占用的內存大小和百分比。主界面如下圖所示:

圖中Shallow Heap 和 Retained Heap分別表示對象自身不包含引用的大小和對象自身並包含引用的大小,具體請參考下面 Shallow Heap 和 Retained Heap 部分的內容。默認的大小單位是 Bytes,可以在 Window - Preferences 菜單中設置單位,圖中設置的是KB。
通過直方圖視圖可以很容易找到占用內存最多的幾個類(通過Retained Heap排序),還可以通過其他方式進行分組(見下圖):

如果存在內存溢出,時間久了溢出類的實例數量或者內存占比會越來越多,排名也越來越靠前。可以點擊工具類上的
圖標進行對比,通過多次對比不同時間點下的直方圖對比就很容易把溢出的類找出來。
Dominator Tree 支配樹
點擊工具欄上的
圖標可以打開Dominator Tree(支配樹)視圖,在此視圖中列出了每個對象(Object Instance)與其引用關系的樹狀結構,同時包含了占用內存的大小和百分比。

通過Dominator Tree視圖可以很容易的找出占用內存最多的幾個對象(根據Retained Heap或Percentage排序),和Histogram類似,也可以通過不同的方式進行分組顯示。
定位溢出源
Histogram視圖和Dominator Tree視圖的角度不同,前者是基於類的角度,后者是基於對象實例的角度,並且可以更方便的看出其引用關系。
首先,在兩個視圖中找出疑似溢出的對象或者類(可以通過Retained Heap排序,並且可以在Class Name中輸入正則表達式的關鍵詞只顯示指定的類名),然后右鍵選擇Path To GC Roots(Histogram中沒有此項)或Merge Shortest Paths to GC Roots,然后選擇 exclude all phantom/weak/soft etc. reference:

GC Roots意為GC根節點,其含義見上面的 GC Roots和引用鏈部分,后面的 exclude all phantom/weak/soft etc. reference 意思是排除虛引用、弱引用和軟引用,即只剩下強引用,因為除了強引用之外,其他的引用都可以被JVM GC掉,如果一個對象始終無法被GC,就說明有強引用存在,從而導致在GC的過程中一直得不到回收,最終就內存溢出了。
通過結果就可以很方便的定位到具體的代碼,然后分析是什么原因無法釋放該對象,比如被緩存了或者沒有使用單例模式等等。
舉例,如果是這樣的執行結果:

上圖中保留了大量的VelocitySqlBulder的外部引用,后來查看了代碼,原來每次調用的時候都實例化一個新的對象,由於VelocitySqlBulder類是無狀態的工具類,因此修改為單例方式就可以解決這個問題。
后續觀察
根據上面分析的結果對問題進行處理之后,再對照之前的操作,看看對象是否還再持續增長,如果沒有就說明這個地方的問題已經解決了。
最后再用 jstat 持續跟蹤一段時間,看看Old和Perm區的內存是否最終穩定在一個范圍之內,如果長時間穩定在一個范圍說明溢出問題得到了解決,否則還要繼續進行分析和處理,一直到穩定為止。
