Java記憶體分析利器MAT使用詳解

itindex發表於2015-08-21

這是一篇閱讀MAT helper的筆記。Heap dump是java程式在特定時間的一個記憶體快照。通常在觸發heap dump之前會進行一次full gc,這樣dump出來的內容就包含的是被gc後的物件。

dump檔案包含的內容:

1,全部的物件:類,域,原生值和引用;

2,全部的類:classloader,類名,超類,靜態域;

3,GC root:被JVM定義的可觸達的物件;

4,執行緒棧和本地變數:執行緒的call stack,本地物件每幀的資訊。

dump檔案不包含記憶體的分配資訊,因此無法查詢誰建立了哪個物件這樣的資訊。

Shallow heap是一個物件佔用的記憶體空間,一個物件需要32或者64bits。

Retained set of X是X在被jvm gc回收後被remove的一組object。

Retained heap of X是在retained set of X中的所有物件的shallow heap size的和。換句話說就是保持X活著需要的記憶體空間。

通俗的講,shallow heap是一個物件在記憶體中的實際空間,而retained heap是一個物件被gc回收後記憶體釋放出來的空間。

這張圖可以看懂什麼是leading set什麼是retained set。

Dominator tree:定義一個物件x dominate 物件y,當每一條從root開始到y的路徑都經過x。說白了就是隻要有y物件的存活,那麼一定會有一個x物件。Dominator tree就是將物件引用圖轉換成的樹形結構。幫助發現在物件間保持alive的依賴,同時也能識別出retained記憶體的最大的chunk。 Immediate dominator x of y是離y最近的dominator。

Dominator tree有幾個屬性:

1,物件x的子樹包含的物件(x dominate的物件集),代表了x的retained set;

2,如果x是y的immediate dominator,那麼x的immediate dominator同樣dominate y,以此類推;

3,dominate tree中的邊不代表物件引用圖裡對應的邊,並非嚴格的直接的物件引用。

這張圖反應了一個物件引用圖轉換成dominator tree的示例。

Gc root:一個gc根就是一個物件,這個物件從堆外可以訪問讀取。以下一些方法可以使一個物件成為gc根。

1,System class:被Bootstrap或者system類載入器載入的類,比如rt.jar裡的java.util.*;

2,JNI local:native程式碼裡的local變數,比如使用者定義的JNI程式碼和JVM的內部程式碼;

3,JNI global:native程式碼裡的global變數;

4,Thread block:當前活躍的執行緒block中引用的物件;

5,Thread:已經啟動並且沒有stop的執行緒;

6,busy monitor:被呼叫了wait()或者notify()或者被synchronized同步的物件,如果是synchronized方法,那麼靜態方法指的類,非靜態方法指的是物件;

7,java local:local變數,比如方法的入參和方法內建立的變數;

8,native stack:native程式碼裡的出入引數,比如file/net/IO方法以及反射的引數;

9,finalizable:在一個佇列裡等待它的finalizer 執行的物件;

10,unfinalized:一個有finalize方法的物件,還沒有被finalize,同時也沒有進入finalizer佇列等待finalize;

11,unreachable:不會被觸碰到的物件,在MAT裡被標記為root用來retain object,否則是不會在分析中出現的;

12,java stack frame:java棧幀包含了本地變數,當dump被解析時且在preferences裡設定過把棧幀當做物件,這時才會產生;

13,unknown:位置的root型別。

接下來是一些獲取dump的方法:

1,在oom時dump:JVM引數:-XX:+HeapDumpOnOutOfMemoryError

2,互動式環境下dump:

1)JVM引數:-XX:+HeapDumpOnCtrlBreak

2)用外部tools:jmap -dump:format=b,file=<filename.hprof> <pid>

3)用外部tools:jconsole

4)用外部工具:MAT

5)kill -3 <pid>

6)jstack -l <pid> > <dumpfile>

一些排查方法:

1,通過top consumers查詢大物件,可以按照class、classloader和package進行group by;

2,通過immediate dominator找到責任物件,對於快速定位一組物件的持有者非常有用,這個操作直接解決了“誰讓這些物件alive”的問題,而不是“誰有這些物件的引用”的問題,更直接高效;

3,執行classloader分析,這個重要性體現在亮點:第一,應用使用不同的classloader載入類,第二,不同 classloader載入的類儲存在不同的永久代,這理論上也是可以被回收的。當有一個類被不同的classloader載入時,這時要根據各自 loader下的instance數量判斷哪個loader更重要,從而要把另一個回收掉;

4,分析執行緒,本身heap dump裡包含了thread資訊,可以通過MAT來檢視threads 的overview和detail,detail中有執行緒的堆記憶體資訊,也有執行緒棧,同時還包含了作業系統本地棧。假設不做heap dump,我們檢查到系統有問題,如何通過執行緒的角度來排查呢?首先top -H -p <pid>以執行緒的模式檢視java應用的執行情況,找到佔用cpu或者記憶體大的執行緒,記錄執行緒id,然後printf %x <tid>轉為16進位制,再jstack -l <pid> > thread.log把java程式的thread dump出來,從裡面找到tid,分析是哪個執行緒佔用了系統資源。

5,分析java容器類,因為java的容器類是最常用來儲存物件的,所以理論上發生記憶體洩露的風險也最高。可以從幾個角度來 看:1)array填充率查詢(填充率fill ratio是陣列中非空元素的比例),列印非原生型別陣列的填充率頻率分佈,從而排查系統中array的利用率;2)陣列按照size分組查詢,列印一個 按size分組的直方圖;3)collection的填充率查詢,ArrayList/HashMap/Hashtable/Properties /Vector/WeakHashMap/ConcurrentHashMap$Segment;4)collection按照size分組直方圖;5) 檢視一個list裡的所有物件;6)檢視hashmap裡的所有物件;7)檢視hashset裡的物件;8)檢查map的碰撞率;9)檢查所有隻有一個常 量的array。

6,分析Finalizer,1)查詢finalizer正在處理的物件;2)查詢finalizer準備處理的物件;3)直接檢視finalizer執行緒;4)檢視finalizer執行緒的thread local物件。

相關文章