如果我們在使用某些大的物件、集合物件或者一些三方包裡的資源,忘記及時釋放資源的話,還是會造成JVM的記憶體洩漏或記憶體浪費的問題。因此,如果想成為更高階的Java開發工程師,我們需要瞭解常見的問題排查的辦法和工具,這個系列的文章,準備介紹一個用來做JVM堆記憶體分析的工具——MAT(Memory Aanlysis Tool)。
MAT的官網在:https://www.eclipse.org/mat/,可以看下它的介紹——MAT是一款高效能、具備豐富功能的Java堆記憶體分析工具,可以用來排查記憶體洩漏和記憶體浪費的問題。
MAT的安裝和設定
MAT 支援兩種安裝方式,一種是”單機版“的,也就是說使用者不必安裝 Eclipse IDE 環境,MAT 作為一個獨立的 Eclipse RCP 應用執行;另一種是”整合版“的,也就是說 MAT 也可以作為 Eclipse IDE 的一部分,和現有的開發平臺整合。
這裡我們考慮獨立安裝,在觀望的下載頁面,選擇mac os版本的安裝檔案下載即可。
-
啟動直接報錯,系統預設的workspace是隻讀的,更換掉即可。怎麼更換呢,在檔案
/Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini
中進行修改。 -
啟動後,UI介面沒反應,參考:https://www.eclipse.org/forums/index.php/t/1090889/,換個包即可。這個問題我遇到過很多次。
配置mat的堆記憶體大小
我的電腦是8C16G的,那理論上分析10G的堆檔案沒問題,但是MAT預設的配置沒有這麼大,需要在/Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini
檔案中進行修改。如下圖所示,我將我的MAT自己的執行時堆記憶體配置成了6G。
配置MAT的使用
MAT的配置頁面可以從Window——>Preferences找到,如下圖所示。
MAT的一般配置有幾個選項
-
Keep unreachable objects:如果勾選這個,則在分析的時候會包含dump檔案中的不可達物件;
-
Hide the getting started wizard:隱藏分析完成後的首頁,控制是否要展示一個對話方塊,用來展示記憶體洩漏分析、消耗最多記憶體的物件排序。
-
Hide popup query help:隱藏彈出查詢幫助,除非使用者透過F1或Help按鈕查詢幫助。
-
Hide Welcome screen on launch:隱藏啟動時候的歡迎介面
-
Bytes Display:設定分析結果中記憶體大小的展示單位
可以看出,MAT不僅支援HPROF檔案的分析,還支援DTFJ檔案的分析。一般sun公司系列的JVM生成的dump檔案都是HPROF格式的,IBM的JVM生成的dump檔案時DTFJ格式的。
基本概念
Heap Dump是Java行程在某個時刻的記憶體快照,不同JVM的實現的Heap Dump的檔案格式可能不同,進而儲存的資料也可能不同,但是一般來說。
Heap Dump中主要包含當生成快照時堆中的java物件和類的資訊,主要分為如下幾類:
-
物件資訊:類名、屬性、基礎型別和取用型別
-
類資訊:類載入器、類名稱、超類、靜態屬性
-
gc roots:JVM中的一個定義,進行垃圾收集時,要遍歷可達物件的起點節點的集合
-
執行緒棧和區域性變數:快照生成時候的執行緒呼叫棧,和每個棧上的區域性變數
Heap Dump中沒有包含物件的分配資訊,因此它不能用來分析這種問題:一個物件什麼時候被建立、一個物件時被誰建立的。
Shallow heap是一個物件本身佔用的堆記憶體大小。一個物件中,每個取用佔用8或64位,Integer佔用4位元組,Long佔用8位元組等等。
Retained set,對於某個物件X來說,它的Retained set指的是——如果X被垃圾收集器回收了,那麼這個集合中的物件都會被回收,同理,如果X沒有被垃圾收集器回收,那麼這個集合中的物件都不會被回收。
Retained heap,物件X的Retained heap指的時候它的Retained set中的所有物件的Shallow si的和,換句話說,Retained heap指的是物件X的保留記憶體大小,即由於它的存活導致多大的記憶體也沒有被回收。
leading set,物件X可能不止有一個,這些物件統一構成了leading set。如果leading set中的物件都不可達,那麼這個leading set對應的retained set中的物件就會被回收。一般有以下幾種情況:
-
某個類的所有實體物件,這個類物件就是leading object
-
某個類記載器載入的所有類,以及這些類的實體物件,這個類載入器物件就是leading object
-
一組物件,要達到其他物件的必經路徑上的物件,就是leading object
在下麵這張圖中,A和B是gc roots中的節點(方法引數、區域性變數,或者呼叫了wait()、notify()或synchronized()的物件)等等。可以看出,E的存在,會導致G無法被回收,因此E的Retained set是E和G;C的存在,會導致E、D、F、G、H都無法被回收,因此C的Retined set是C、E、D、F、G、H;A和B的存在,會導致C、E、D、F、G、H都無法被回收,因此A和B的Retained set是A、B、C、E、D、F、G、H。
MAT根據堆上的物件取用關係構建了支配樹(Dominator Tree),透過支配樹可以很方便得識別出哪些物件佔用了大量的記憶體,並可以看到它們之間的依賴關係。
如果在物件圖中,從gc root或者x上游的一個節點開始遍歷,x是y的必經節點,那麼就可以說x支配了y(dominate)。
如果在物件圖中,x支配的所有物件中,y的距離最近,那麼就可以說x直接支配(immediate dominate)y。
支配樹是基於物件的取用關係圖建立的,在支配樹中每個節點都是它的子節點的直接支配節點。基於支配樹可以很清楚得看到物件之間的依賴關係。
現在看個例子,在下麵這張圖中
-
x節點的子樹就是所有被x支配的節點集合,也正式x的retained set;
-
如果x是y的直接支配節點,那麼x的支配節點也可以支配y
-
支配樹中的邊跟物件取用圖中的取用關係並不是一一對應的。
在MAT中,gc roots的概念跟研究垃圾收集演演算法時候的概念稍微有點不同。gc roots中的物件,是指那些可以從堆外訪問到的物件的集合。如果一個物件符合下麵這些場景中的一個,就可以被認為是gc roots中的節點:
-
System Class:由bootstrap classloader載入的類,例如rt.jar,裡面的類的包名都是
java.util.*
開頭的。 -
JNI Local:native程式碼中的區域性變數,例如使用者編寫的JNI程式碼或JVM內部程式碼。
-
JNI Global:native程式碼中的全域性變數,例如使用者編寫的JNI程式碼或JVM內部程式碼。
-
Thread Block:被當前活躍的執行緒鎖取用的物件。
-
Thread:正在存活的執行緒
-
Busy Monitor:呼叫了wait()、notify()或synchronized關鍵字修飾的程式碼——例如
synchronized(object)
或synchronized
方法。 -
Java Local:區域性變數。例如函式的輸入引數、正在執行的執行緒棧裡建立的物件。
-
Native Stack:native程式碼的輸入或輸出引數,例如使用者定義的JNI程式碼或JVM的內部程式碼。在檔案/網路IO方法或反射方法的引數。
-
Finalizable:在finalize佇列中等待它的finalizer物件執行的物件。
-
Unfinalized:多載了finalize方法,但是還沒有進入finalize佇列中的物件。
-
Unreachable:從任何gc roots節點都不可達的物件,在MAT中將這些物件視為root節點,如果不這麼做,就不能對這些物件進行分析。
-
Java Stack Frame:Java棧幀,用於存放區域性變數。只在dump檔案被解析的時候會將java stack frame視為物件。
-
Unknown:沒有root型別的物件。有些dump檔案(例如IBM的Portable Heap Dump)沒有root資訊。
獲取Dump檔案
-
透過MAT生成dump檔案 透過這個路徑找到生成dump檔案的對話方塊 選擇一個行程,點選finish即可
-
透過jmap命令生成dump檔案
-
命令格式:
jmap -dump:live,format=b,file=heap.bin
-
註意:如果要保留heapdump中的不可達物件,則需要把”:live“去掉,即使用命令”jmap -dump,format=b,file=heap.bin “
-
透過設定JVM引數自動生成 使用
-XX:+HeapDumpOnOutOfMemoryError
這個JVM引數,在Java行程執行過程中發生OOM的時候就會生成一個heapdump檔案,並寫入到指定目錄,一般用-XX:HeapDumpPath=${HOME}/logs/test
來設定。
本號專註於後端技術、JVM問題排查和最佳化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。