java應用監測(6)-第三方記憶體分析工具MAT
tags: java,troubleshooting,monitor,mat
一句話概括:
MAT
是一個強大的記憶體分析工具,可以快捷、有效地幫助我們找到記憶體洩露,減少記憶體消耗分析工具,下文將進行講解。
1 引言
之前的文章有提過,記憶體中堆的使用情況是應用效能監測的重點,而對於堆的快照,可以dump出來進一步分析,總的來說,一般我們對於堆dump快照有三種方式:
- 新增啟動引數發生OOM時自動dump:
java應用的啟動引數一般最好都加上
-XX:+HeapDumpOnOutOfMemoryError
及-XX:HeapDumpPath=logs/heapdump.hprof
,即在發生OOM時自動dump堆快照,但這種方式相當來說是滯後的(需要等到發生OOM後)。 - 使用命令按需手動dump:
我們也可以使用
jmap -dump:format=b,file=HeapDump.hprof <pid>
工具手動進行堆dump和執行緒dump - 使用工具手動dump:
如上一篇文章對
jconsole
及jvisualvm
的講解中講到,jvisualvm有提供dump堆快照的功能,點選一下即可。
那麼,對於dump出來的檔案,如何對它們進行分析?jvisualvm
可以直接裝入快照檔案進行分析,而本文所介紹的MAT
,相對來說記憶體分析功能更強大,它可以自動檢測有可能發生問題(特別是記憶體溢位、記憶體洩露)的地方,也可以詳細檢視類記憶體佔用情況,方法級的呼叫情況等,是一個分析記憶體狀況的不可多得的工具。
2 MAT工具介紹
MAT(Memory Analyzer tool)是一款記憶體分析器工具,它是一個快速且功能豐富的堆記憶體分析器,幫助我們查詢記憶體洩漏和分析高記憶體消耗問題。官方網站是:https://www.eclipse.org/mat/
,有興趣可以上去看一下。使用MAT,可以輕鬆實現以下功能:
- 找到最大的物件,因為MAT提供顯示合理的累積大小(
retained size
) - 探索物件圖,包括
inbound
和outbound
引用,即引用此物件的和此物件引出的。 - 查詢無法回收的物件,可以計算從垃圾收集器根到相關物件的路徑
- 找到記憶體浪費,比如冗餘的String物件,空集合物件等等
3 MAT工具安裝
MAT
安裝有兩種方式,一種是以eclipse
外掛方式安裝,一種是獨立安裝。在MAT
的官方檔案中有相應的安裝檔案下載,下載地址為:https://www.eclipse.org/mat/downloads.php
- 若使用eclipse外掛安裝,
help -> install new soft
點選ADD
,在彈出框中新增外掛地址:http://download.eclipse.org/mat/1.9.0/update-site/
,也可以直接在下載頁面下載離線外掛包,以離線方式安裝。 - 獨立安裝,windows下,根據系統直接下載
Windows (x86)
或Windows (x86_64)
,下載時可以選擇適合自己的映象,雙擊安裝即可。
4 MAT工具使用
MAT
定位是記憶體分析工具,它的主要功能就是對記憶體快照進行分析,幫助我們找到有可能記憶體溢位或記憶體洩漏的地方,因此,找到佔用記憶體大的物件和找出無法回收的物件是其主要目的。MAT官方檔案,地址如下:https://help.eclipse.org/2019-06/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
,對MAT
的使用進行描述,有興趣的同學可以上去看看。下面主要對MAT
的常用概念、常用的功能進行介紹。
下文中,以
java-monitor-example
(https://github.com/mianshenglee/my-example/tree/master/java-monitor-example)
為例,此示例是一個簡單的spring boot
工程,裡面的一個controller
中的user/oom
介面呼叫的service
物件通過List
成員不斷地新增User
物件,最終導致OOM
的發生,應用的啟動引數是-Xms64m -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/logs/heapdump.hprof
。
4.1 MAT相關概念說明
4.1.1 記憶體洩漏與記憶體溢位
- 記憶體洩露:物件已經沒用了(不被任何程式邏輯所需要),還存在被根元素引用的情況,無法通過垃圾收集器進行自動回收,需要通過找出洩漏的程式碼位置和原因,才好確定解決方案;
- 記憶體溢位:記憶體中的物件都還存活著,JVM的堆分配空間不足,需要檢查堆設定大小(-Xmx與-Xms),程式碼是否存在物件生命週期太長、持有狀態時間過長的情況。
4.1.2 引用(強引用,軟引用,弱引用,虛引用)
-
Strong Ref
(強引用):強可達性的引用,物件儲存在記憶體中,只有去掉強可達,物件才被回收,通常我們編寫的程式碼都是Strong Ref。 -
Soft Ref
(軟引用):對應軟可達性,只要有足夠的記憶體,就一直保持物件,直到發現記憶體吃緊且沒有Strong Ref時才回收物件。一般可用來實現快取,通過java.lang.ref.SoftReference類實現。 -
Weak Ref
(弱引用):比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收物件而不必等到記憶體吃緊的時候。通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現。 -
Phantom Ref
(虛引用):根本不會在記憶體中保持任何物件,你只能使用Phantom Ref本身。一般用於在進入finalize()方法後進行特殊的清理過程,通過 java.lang.ref.PhantomReference實現。
4.1.3 shallow heap
及retained heap
-
shallow heap
:物件本身佔用記憶體的大小,也就是物件頭加成員變數(不是成員變數的值)的總和,如一個引用佔用32或64bit,一個integer佔4bytes,Long佔8bytes等。如簡單的一個類裡面只有一個成員變數int i
,那麼這個類的shallo size
是12位元組,因為物件頭是8位元組,成員變數int
是4位元組。常規物件(非陣列)的Shallow size有其成員變數的數量和型別決定,陣列的shallow size有陣列元素的型別(物件型別、基本型別)和陣列長度決定。 -
retained heap
:如果一個物件被釋放掉,那會因為該物件的釋放而減少引用進而被釋放的所有的物件(包括被遞迴釋放的)所佔用的heap大小,即物件X被垃圾回收器回收後能被GC從記憶體中移除的所有物件之和。相對於shallow heap,Retained heap可以更精確的反映一個物件實際佔用的大小(若該物件釋放,retained heap都可以被釋放)。
4.1.4 outgoing references
與incoming references
-
outgoing references
:表示該物件的出節點(被該物件引用的物件)。 -
incoming references
:表示該物件的入節點(引用到該物件的物件)。
4.1.5 Dominator Tree
將物件樹轉換成Dominator Tree
能幫助我們快速的發現佔用記憶體最大的塊,也能幫我們分析物件之間的依賴關係。Dominator Tree
有以下幾個定義:
- 物件X
Dominator
(支配)物件Y,當且僅當在物件樹中所有到達Y的路徑都必須經過X - 物件Y的直接
Dominator
,是指在物件樹中距離Y最近的Dominator
-
Dominator tree
利用物件樹構建出來。在Dominator tree
中每一個物件都是他的直接Dominator
的子節點。
物件樹和Dominator tree
的對應關係如下:
如上圖,因為A和B都引用到C,所以A釋放時,C記憶體不會被釋放。所以這塊記憶體不會被計算到A或者B的Retained Heap中,因此,物件樹在轉換成Dominator tree
時,會A、B、C三個是平級的。
4.1.6 Garbage Collection Roots(GC root)
在執行GC時,是通過物件可達性來判斷是否回收物件的,一個物件是否可達,也就是看這個物件的引用連是否和GC Root
相連。一個GC root
指的是可以從堆外部訪問的物件,有以下原因可以使一個物件成為GC root
物件。
-
System Class
: 通過bootstrap/system類載入器載入的類,如rt.jar中的java.util.* -
JNI Local
: JNI方法中的變數或者方法形參 -
JNI Global
:JNI方法中的全域性變數 -
Thread Block
:執行緒裡面的變數,一個活著的執行緒裡面的物件肯定不能被回收 -
Thread
:處於啟用狀態的執行緒 -
Busy Monitor
:呼叫了wait()、notify()方法,或者是同步物件,例如呼叫synchronized(Object) 或者進入一個synchronized方法後的當前物件 -
Java Local
:本地變數,例如方法的輸入引數或者是方法內部建立的仍線上程堆疊裡面的物件 -
Native Stack
:Java 方法中的變數或者方法形參. -
Finalizable
:等待執行finalizer的物件 -
Unfinalized
:有finalize方法,但未進行finalized,且不在finalizer佇列的物件。 -
Unreachable
:通過其它root都不可達的物件,MAT會把它標記為root以便於分析回收。 -
Java Stack Frame
:java棧幀 Unknown
4.2 Leak Suspects自動分析洩漏
當發生OOM
獲取到heapdump.hprof
檔案或者手動dump出文件後,使用MAT
開啟檔案。開啟後預設會提示是否進行記憶體洩漏檢測報告(如果開啟Dump時跳過了的話,也可以從其它入口進入工具欄上的 Run Expect System Test -> Leak Suspects
),如下圖所示:
選擇是後進入報告內容,此報告內容會幫我們分析的可能有記憶體洩露嫌疑的地方,它會把累積記憶體佔用較大的通過餅狀圖顯示出來。如下圖所示:
如上圖,報告已指出UserService
佔用了76.73%的記憶體,而這些記憶體都在Object[]這個陣列中。因此,很大一種可能是這個物件中的陣列數量過大。點選Details
可以檢視更詳細的內容:
在此詳細而,Shortest Paths To the Accumulation Point
可以顯示出到GC roots
的最短路徑,由此可以分析是由於和哪個GC root
相連導致當前Retained Heap
佔用相當大的物件無法被回收,本示例中,GC root
是執行緒的本地變數(java local
)。Accumulated Objects in Dominator Tree
以Dominator Tree
為檢視,可以方便的看出受當前物件“支配”的物件中哪個佔用Retained Heap比較大。圖中可看出物件都在ArrayList
中,而ArrayList
下有Object
陣列,陣列下是User
物件。此可以知道問題出在哪裡了。需要針對這個位置,檢視程式碼,找出導致這個陣列儲存的User
過量的原因。
注:在原始堆轉儲檔案的目錄下,
MAT
已經將報告的內容壓縮打包到一個zip檔案,命名為“xxx_Leak_Suspects.zip”,整個報告是一個HTML格式的檔案,可以用瀏覽器直接開啟檢視,可以方便進行報告分發、分享。
4.3 histogram檢視檢視物件個數與大小
點選Overview
頁面Actions
區域內的“Histogram檢視”或點選工具欄的“histogram”按鈕,可以顯示直方圖列表,它以Class類的維度展示每個Class類的例項存在的個數、 佔用的Shallow heap
和 Retained記憶體
大小,可以分別排序顯示。如下圖所示:
Shallow Heap
與Retained Heap
的概念前面已經講過。
對於java的物件,其成員基本都是引用。真正的記憶體都在堆上,看起來是一堆原生的byte[],char[],int[],因此如果只看物件本身的記憶體,數量都很小,而多數情況下,在Histogram檢視看到例項物件數量比較多的類都是一些基礎型別(通常都排在前面),如char[]、String、byte[],所以僅從這些是無法判斷出具體導致記憶體洩露的類或者方法的。從上圖中,可以直接看到User
物件的數量很多,有時不容易看出來的,可以使用工具欄中的group result by
可以以super class
,class loader
等排序,需要特別注意自定義的classLoader
,如下圖:
通常,如果Histogram檢視展示的數量多的例項物件不是基礎型別,而是使用者自定義的類或者有嫌疑的類,就要重點關注檢視。想進一步分析的,可以右鍵,選擇使用List objects
或 Merge Shortest Paths to GC roots
等功能繼續鑽取資料。其中list objects
分別有outgoing references
及incoming references
,可以找出由這個物件出去的引用及通過哪些引用到這個物件的。Merge Shortest Paths to GC roots
可以排除掉所有不是強引用的,找到這個物件的到GC root
的引用路徑。最終目的就是找到佔用記憶體最大物件和無法回收的物件,計算從垃圾收集器根到相關物件的路徑,從而根據這個物件路徑去檢查程式碼,找出問題所在。
4.4 Dominator Tree
檢視
點選Overview
頁面Actions
區域內的“Dominator Tree檢視”或點選工具欄的“open Dominator Tree”按鈕,可以進入 Dominator Tree
檢視。該檢視以例項物件的維度展示當前堆記憶體中Retained Heap
佔用最大的物件,以及依賴這些物件存活的物件的樹狀結構。如下圖:
檢視展示了例項物件名、Shallow Heap
大小、Retained Heap
大小、以及當前物件的Retained Heap
在整個堆中的佔比。該圖是樹狀結構的,當上一級的物件被回收,那麼,它引用的子物件都會被回收,這也是Dominator
的意義,當父節點的回收會導致子節點也被回收。通過此檢視,可以很方便的找出佔用Retained Heap
記憶體最多的幾個物件,並表示出某些objects的是因為哪些objects的原因而存活。本示例中,可以看出是由於UserService
中的ArrayList
引用的陣列儲存過量的User
物件。
4.5 執行緒檢視檢視執行緒棧執行情況
點選工具欄的“齒輪”按鈕,可以開啟Thread Overview
檢視,可以檢視執行緒的棧幀資訊,包括執行緒物件/執行緒棧資訊、執行緒名、Shallow Heap
、Retained Heap
、類載入器、是否Daemon執行緒等資訊。結合記憶體Dump分析,看到執行緒棧幀中的本地變數,在左下方的物件屬性區域還能看到本地變數的屬性值。如按上面的示例(shortest paths to GC root
),知道是由於執行緒Thread-12
是GC-root
佔用記憶體大,線上程檢視中,就可以著重看它的屬性情況,如下圖:
由上圖可以看到此執行緒呼叫WithOOM
方法,使用了變數UserService
,而變數使用了userList
,它包含了大量的User
物件,佔用retained heap
很大。
5 總結
對於java應用的記憶體分析,需要對java應用的記憶體進行dump操作,生成記憶體快照後,使用MAT
進行分析,找出大物件,找到記憶體洩漏或溢位的地方,從而分析程式碼,解決問題。本文對MAT
的使用場景,基本概念,安裝、使用進行了詳細介紹,大家可以自己安裝一下,寫個小示例或者拿本文中的示例,實踐一下。通過本文,希望可以幫助大家更方便,更有效率地分析記憶體,解決java應用的記憶體故障問題。
引數資料
-
MAT
官網:https://www.eclipse.org/mat/
-
MAT
下載:http://www.eclipse.org/mat/downloads.php
-
MAT
使用手冊:https://help.eclipse.org/2019-06/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
- Shallow and retained sizes:
https://www.yourkit.com/docs/java/help/sizes.jsp
- 使用Eclipse Memory Analyzer Tool(MAT)分析線上故障(一) - 檢視&功能篇:
https://www.cnblogs.com/trust-freedom/p/6744948.html
- 示例程式碼地址:
https://github.com/mianshenglee/my-example/tree/master/java-monitor-example