1. 程式人生 > 程式設計 >java應用監測(6)-第三方記憶體分析工具MAT

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: 如上一篇文章jconsolejvisualvm的講解中講到,jvisualvm有提供dump堆快照的功能,點選一下即可。

那麼,對於dump出來的檔案,如何對它們進行分析?jvisualvm可以直接裝入快照檔案進行分析,而本文所介紹的MAT,相對來說記憶體分析功能更強大,它可以自動檢測有可能發生問題(特別是記憶體溢位、記憶體洩露)的地方,也可以詳細檢視類記憶體佔用情況,方法級的呼叫情況等,是一個分析記憶體狀況的不可多得的工具。

2 MAT工具介紹

MAT(Memory Analyzer tool)是一款記憶體分析器工具,它是一個快速且功能豐富的堆記憶體分析器,幫助我們查詢記憶體洩漏和分析高記憶體消耗問題。官方網站是:https://www.eclipse.org/mat/,有興趣可以上去看一下。使用MAT,可以輕鬆實現以下功能:

  • 找到最大的物件,因為MAT提供顯示合理的累積大小(retained size
  • 探索物件圖,包括inboundoutbound引用,即引用此物件的和此物件引出的。
  • 查詢無法回收的物件,可以計算從垃圾收集器根到相關物件的路徑
  • 找到記憶體浪費,比如冗餘的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 heapretained 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 referencesincoming 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 TreeDominator 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 heapRetained記憶體大小,可以分別排序顯示。如下圖所示:

Shallow HeapRetained Heap的概念前面已經講過。 對於java的物件,其成員基本都是引用。真正的記憶體都在堆上,看起來是一堆原生的byte[],char[],int[],因此如果只看物件本身的記憶體,數量都很小,而多數情況下,在Histogram檢視看到例項物件數量比較多的類都是一些基礎型別(通常都排在前面),如char[]、String、byte[],所以僅從這些是無法判斷出具體導致記憶體洩露的類或者方法的。從上圖中,可以直接看到User物件的數量很多,有時不容易看出來的,可以使用工具欄中的group result by 可以以super class,class loader等排序,需要特別注意自定義的classLoader,如下圖:

通常,如果Histogram檢視展示的數量多的例項物件不是基礎型別,而是使用者自定義的類或者有嫌疑的類,就要重點關注檢視。想進一步分析的,可以右鍵,選擇使用List objectsMerge Shortest Paths to GC roots等功能繼續鑽取資料。其中list objects分別有outgoing referencesincoming 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 HeapRetained Heap、類載入器、是否Daemon執行緒等資訊。結合記憶體Dump分析,看到執行緒棧幀中的本地變數,在左下方的物件屬性區域還能看到本地變數的屬性值。如按上面的示例(shortest paths to GC root),知道是由於執行緒Thread-12GC-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

相關閱讀