1. 程式人生 > >記憶體洩漏排查 MAT工具使用

記憶體洩漏排查 MAT工具使用

説在前面:

儘管Java虛擬機器可以幫我們對記憶體進行回收,但是其回收的是Java虛擬機器不再引用的物件。

很多時候我們使用系統的IO流,Cursor,Receiver如果不及時釋放,就會導致記憶體洩漏,這些場景是常見的,一般開發人員也都能夠避免。

但是,很多時候記憶體洩漏的現象不是很明顯,

1.比如內部類,Handler相關的使用導致的記憶體洩漏,

2.或者你使用了第三方library的一些引用,比較消耗資源,但又不是像系統資源那樣會引起你足夠的注意去手動釋放它們。

當代碼越來越多,如果結構不是很清晰,即使是常見的資源也有可能略掉,從而導致記憶體洩漏。

記憶體洩漏很有可能會導致記憶體溢位,就是常說的OOM,從而導致應用crash,給使用者一種糟糕的體驗。

最後通過記憶體洩漏分析,集合使用率,Hash效能分析,OQL快讀定位空集合實戰演示如何在實際應用中使用MAT。(通過一些靜態檢測也可以在開發期發現一些記憶體洩漏的問題,後面會有一些靜態檢測的文章)

造成OutOfMemoryError原因一般有2種:

1、記憶體洩露,物件已經死了,無法通過垃圾收集器進行自動回收,通過找出洩露的程式碼位置和原因,才好確定解決方案;

2、記憶體溢位,記憶體中的物件都還必須存活著,這說明Java堆分配空間不足,檢查堆設定大小(-Xmx與-Xms),檢查程式碼是否存在物件生命週期太長、持有狀態時間過長的情況。

#一 相關概念 

Java虛擬機器如何判定記憶體洩漏的呢?下面介紹一些相關概念

這裡也要說明一下Java的引用規則:從最強到最弱,不同的引用(可到達性)級別反映了物件的生命週期。* 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實現。


##1.1 GC Root ##
JAVA虛擬機器通過可達性(Reachability)來判斷物件是否存活,基本思想:以”GC Roots”的物件作為起始點向下搜尋,搜尋形成的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(即不可達的),則該物件被判定為可以被回收的物件,反之不能被回收。

GC Roots可以是以下任意物件

  • 一個在current thread(當前執行緒)的call stack(呼叫棧)上的物件(例如方法引數和區域性變數)
  • 執行緒自身或者system class loader(系統類載入器)載入的類
  • native code(原生代碼)保留的活動物件

##1.2 記憶體洩漏
物件無用了,但仍然可達(未釋放),垃圾回收器無法回收。

##1.3 強(strong)、軟(soft)、弱(weak)、虛(phantom)引用 ##

Strong references

普通的java引用,我們通常new的物件就是:
StringBuffer buffer = new StringBuffer();
如果一個物件通過一串強引用鏈可達,那麼它就不會被垃圾回收。你肯定不希望自己正在使用的引用被垃圾回收器回收吧。但對於集合中的物件,應在不使用的時候移除掉,否則會佔用更多的記憶體,導致記憶體洩漏。

Soft reference
當物件是Soft reference可達時,gc會向系統申請更多記憶體,而不是直接回收它,當記憶體不足的時候才回收它。因此Soft reference適合用於構建一些快取系統,比如圖片快取。

WeakReference

WeakReference不會強制物件儲存在記憶體中。它擁有比較短暫的生命週期,允許你使用垃圾回收器的能力去權衡一個物件的可達性。在垃圾回收器掃描它所管轄的記憶體區域過程中,一旦gc發現物件是weakReference可達,就會把它放到ReferenceQueue中,等下次gc時回收它。
WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);
系統為我們提供了WeakHashMap,和HashMap類似,只是其key使用了weak reference。如果WeakHashMap的某個key被垃圾回收器回收,那麼entity也會自動被remove。

由於WeakReference被GC回收的可能性較大,因此,在使用它之前,你需要通過weakObj.get()去判斷目的物件引用是否已經被回收.

Reference queque

一旦WeakReference.get()返回null,它指向的物件就會被垃圾回收,那麼WeakReference物件就沒有用了,意味著你應該進行一些清理。比如在WeakHashMap中要把回收過的key從Map中刪除掉,避免無用的的weakReference不斷增長。
ReferenceQueue可以讓你很容易地跟蹤dead references。WeakReference類的建構函式有一個ReferenceQueue引數,當指向的物件被垃圾回收時,會把WeakReference物件放到ReferenceQueue中。這樣,遍歷ReferenceQueue可以得到所有回收過的WeakReference。

Phantom reference

和soft,weak Reference區別較大,它的get()方法總是返回null。這意味著你只能用PhantomReference本身,而得不到它指向的物件。當WeakReference指向的物件變得弱可達(weakly reachable)時會立即被放到ReferenceQueue中,這在finalization、garbage collection之前發生。理論上,你可以在finalize()方法中使物件“復活”(使一個強引用指向它就行了,gc不會回收它)。但沒法復活PhantomReference指向的物件。而PhantomReference是在garbage collection之後被放到ReferenceQueue中的,沒法復活。

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

MAT 不是一個萬能工具,它並不能處理所有型別的堆儲存檔案。但是比較主流的廠家和格式,例如 Sun, HP, SAP 所採用的 HPROF 二進位制堆儲存檔案,以及 IBM 的 PHD 堆儲存檔案等都能被很好的解析。

我在實際操作過程中採用的是jmap獲取堆轉儲檔案,然後scp到本地,

載入後首頁如下圖,在首頁上比較有用的是Histogram和Leak Suspects。


1. 用jmap生成堆資訊

100252_ot9E_1767531.png

這樣在E盤的jmap資料夾裡會有一個map.bin的堆資訊檔案 

然後MAT軟體載入

2. 將堆資訊匯入到mat中分析   

101421_bdMx_1767531.png

載入後首頁如下圖,在首頁上比較有用的是Histogram和Leak Suspects。


點選Leak Suspects會在堆轉儲檔案同目錄內生成一個Leak Suspects.zip檔案,同時也會從首頁跳轉到Leak Suspects頁面。


解壓該檔案後可以通過瀏覽器開啟分析結果


下面是Leak Suspects頁面

3. 生成分析

餅圖的每個塊,代表一個可疑問題,滑鼠上去就可以看到下面的內容提示


在Leak Suspects頁面會給出可能的記憶體洩露,如上圖所示有三個可能的記憶體洩露,但是隻有第一個是我程式裡的,另外兩個是jar包或jdk裡面的,這個可以不用管。

點選Details進入詳情頁面。在詳情頁面Shortest Paths To the Accumulation Point表示GC root到記憶體消耗聚集點的最短路徑,如果某個記憶體消耗聚集點有路徑到達GC root,則該記憶體消耗聚集點不會被當做垃圾被回收。


在All Accumulated Objects by Class列舉了該物件所儲存的所有內容。


為了找到記憶體洩露,我獲取了兩個堆轉儲檔案,兩個檔案獲取時間間隔是一天(因為記憶體只是小幅度增長,短時間很難發現問題)。對比兩個檔案的物件,通過對比後的結果可以很方便定位記憶體洩露。

MAT同時開啟兩個堆轉儲檔案,分別開啟Histogram,如下圖。在下圖中方框1按鈕用於對比兩個Histogram,對比後在方框2處選擇Group By package,然後對比各物件的變化。不難發現heap3.hprof比heap6.hprof少了64個eventInfo物件,如果對程式碼比較熟悉的話想必這樣一個結果是能夠給程式設計師一定的啟示的。而我也是根據這個啟示差找到了最終記憶體洩露的位置。

我記憶體洩露位置是一個list,這個list只在這裡一直不停的往裡新增eventInfo物件,卻沒有釋放過

修改後程式碼:

    從上圖可以看到它的大部分功能,在餅圖上,你會發現轉儲的大小和數量的類,物件和類載入器。
正確的下面,餅圖給出了一個印象最大的物件轉儲。移動你的滑鼠一片看到物件中的物件的細節檢查在左邊。下面的Action標籤中:

  • Histogram可以列出記憶體中的物件,物件的個數以及大小。

  • Dominator Tree可以列出那個執行緒,以及執行緒下面的那些物件佔用的空間。

  • Top consumers通過圖形列出最大的object。

  • Leak Suspects通過MA自動分析洩漏的原因。

Histogram

120039_qSi5_1767531.png

  • Class Name : 類名稱,java類名

  • Objects : 類的物件的數量,這個物件被建立了多少個

  • Shallow Heap :一個物件記憶體的消耗大小,不包含對其他物件的引用

  • Retained Heap :是shallow Heap的總和,也就是該物件被GC之後所能回收到記憶體的

總結出來只有一條:存在無效的引用!

良好的模組設計以及合理使用設計模式有助於解決此問題。