天天看點

MAT - Memory Analyzer Tool 使用進階

#前言#

盡管Java虛拟機可以幫我們對記憶體進行回收,但是其回收的是Java虛拟機不再引用的對象。很多時候我們使用系統的IO流,Cursor,Receiver如果不及時釋放,就會導緻記憶體洩漏,這些場景是常見的,一般開發人員也都能夠避免。但是,很多時候記憶體洩漏的現象不是很明顯,比如内部類,Handler相關的使用導緻的記憶體洩漏,或者你使用了第三方library的一些引用,比較消耗資源,但又不是像系統資源那樣會引起你足夠的注意去手動釋放它們。當代碼越來越多,如果結構不是很清晰,即使是常見的資源也有可能略掉,進而導緻記憶體洩漏。記憶體洩漏很有可能會導緻記憶體溢出,就是常說的OOM,進而導緻應用crash,給使用者一種糟糕的體驗。該篇文章就是介紹記憶體分析工具MAT以及實戰來幫你更好的分析記憶體問題。前面是相關概念介紹,最後通過記憶體洩漏分析,集合使用率,Hash性能分析,OQL快讀定位空集合實戰示範如何在實際應用中使用MAT。(通過一些靜态檢測也可以在開發期發現一些記憶體洩漏的問題,後面會有一些靜态檢測的文章)

#一 相關概念

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

##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中的,沒法複活。

關于Phantom reference的更多讨論,請參考:understanding-weak-references

#二 MAT相關視圖和概念

2.1 Shallow Heap

Shallow size就是對象本身占用記憶體的大小,不包含其引用的對象記憶體,實際分析中作用不大。

正常對象(非數組)的ShallowSize由其成員變量的數量和類型決定

數組的shallow size有數組元素的類型(對象類型、基本類型)和數組長度決定

Shallow Size of a String object

class String{
    public final class String {8 Bytes header
    private char value[]; 4 Bytes
    private int offset; 4 Bytes
    private int count; 4 Bytes
    private int hash = 0; 4 Bytes
…}
"Shallow size“ of a String ==24 Bytes
           

java的對象成員都是些引用。真正的記憶體都在堆上,看起來是一堆原生的byte[], char[], int[],對象本身的記憶體都很小。是以我們可以看到以Shallow Heap進行排序的Histogram圖中,排在第一位第二位的是byte,char

2.2 Retained Heap

retained heap值的計算方式是将retained set中的所有對象大小疊加。或者說,由于X被釋放,導緻其它所有被釋放對象(包括被遞歸釋放的)所占的heap大小。

Retained Set

當X被回收時那些将被GC回收的對象集合。

比如:

一個ArrayList持有100,000個對象,每一個占用16 bytes,移除這些ArrayList可以釋放16 x 100,000 + X,X代表ArrayList的shallow大小。相對于shallow heap,RetainedHeap可以更精确的反映一個對象實際占用的大小(因為如果該對象釋放,retained heap都可以被釋放)。

2.3 Histogram

可列出每一個類的執行個體數。支援正規表達式查找,也可以計算出該類所有對象的retained size

MAT - Memory Analyzer Tool 使用進階

2.4 Dominator Tree

Dominator Tree:對象之間dominator關系樹。如果從GC Root到達Y的的所有path都經過X,那麼我們稱X dominates Y,或者X是Y的Dominator Dominator Tree由系統中複雜的對象圖計算而來。從MAT的dominator tree中可以看到占用記憶體最大的對象以及每個對象的dominator。

我們也可以右鍵選擇Immediate Dominator”來檢視某個對象的dominator。

MAT - Memory Analyzer Tool 使用進階

2.5 Path to GC Roots

檢視一個對象到RC Roots的引用鍊

通常在排查記憶體洩漏的時候,我們會選擇exclude all phantom/weak/soft etc.references,

意思是檢視排除虛引用/弱引用/軟引用等的引用鍊,因為被虛引用/弱引用/軟引用的對象可以直接被GC給回收,我們要看的就是某個對象否還存在Strong 引用鍊(在導出HeapDump之前要手動出發GC來保證),如果有,則說明存在記憶體洩漏,然後再去排查具體引用。

MAT - Memory Analyzer Tool 使用進階

###檢視目前Object所有引用,被引用的對象:

####List objects with (以Dominator Tree的方式檢視)

incoming references 引用到該對象的對象

outcoming references 被該對象引用的對象

####Show objects by class (以class的方式檢視)

incoming references 引用到該對象的對象

outcoming references 被該對象引用的對象

2.6 OQL(Object Query Language)

類似SQL查詢語言

Classes:Table

Objects:Rows

Fileds: Cols

select * from com.example.mat.Listener

查找size=0并且未使用過的ArrayList

select * from java.util.ArrayList where size=0 and modCount=0

查找所有的Activity

select * from instanceof android.app.Activity

2.7 記憶體快照對比

方式一:Compare To Another Heap Dump

直接進行比較

MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階

方式二:Compare Baseket

MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階

方式二比較根全面,可以直接給出百分比,而且還有更多比較選項

引出一個同僚開發過程中的一個真實的例子,通過AS的Memory監測,他發現在微信支付完成後記憶體有突然大記憶體飙升的情況,後來通過Compare Baseket進行對比,發現記憶體增大了8M,并通過工具檢視了bitmap的原圖(如何檢視Bitmap原圖,可以參考高建武的文章:打開MAT中的Bitmap原圖)。發現是微信回調頁面一張背景圖檔占用了很大記憶體。

#三 MAT記憶體分析實戰

##實戰一 記憶體洩漏分析

關于如何安裝和導出HeapDump檔案 @高建武 已經寫了,這裡就不啰嗦了,請移步:

Android記憶體優化MAT使用入門

Android記憶體優化MAT使用進階

這裡強調一點就是在導出prof檔案前,先手動出發一次GC,這樣可以確定隻儲存那些無法回收的對象記憶體快照,另外Android Studio提供自動轉換。

查找導緻記憶體洩漏的類

既然環境已經搭好,heap dump也成功倒入,接下來就去分析問題

方式一:

  • 1.查找目标類

    如果在開發過程中,你的目标很明确,比如就是查找自己負責的Activity,那麼通過包名或者Class篩選,OQL搜尋都可以快速定位到

    OQL:

    點選OQL圖示,在視窗輸入

    select * from instanceof android.app.Activity

     并按Ctrl + F5或者!按鈕執行
  • 2.Paths to GC Roots:exclude all phantom/weak/soft etc.references

    檢視一個對象到RC Roots是否存在引用鍊。要将虛引用/弱引用/軟引用等排除,因為被虛引用/弱引用/軟引用的對象可以直接被GC給回收.

  • 3.分析具體的引用為何沒有被釋放,并進行修複

小技巧:

  • 當目的不明确時,可以直接定位到RetainedHeap最大的Object,Select incoming references ,檢視引用鍊,定位到可疑的對象然後Path to GC Roots進行引用鍊分析
  • 如果大對象篩選看不出差別,可以試試按照class分組,再尋找可疑對象進行GC引用鍊分析
  • 直接按照包名直接檢視GC引用鍊,可以一次性篩選多個類,但是如下圖所示,選項是 

    Merge Shortest Path to GCRoots

    ,這個選項具體不是很明白,不過也能篩選出存在GC引用鍊的類,這種方式的準确性還待驗證。
MAT - Memory Analyzer Tool 使用進階

是以有時候進行MAT分析還是需要一些經驗,能夠幫你更快更準确的定位。

##實戰二 集合使用率分析

集合在開發中會經常使用到,如何選擇合适的資料結構的集合,初始容量是多少(太小,可能導緻頻繁擴容),太大,又會開銷跟多記憶體。當這些問題不是很明确時或者想檢視集合的使用情況時,可以通過MAT來進行分析。

###1.篩選目标對象

MAT - Memory Analyzer Tool 使用進階

###2.Show Retained Set(查找當X被回收時那些将被GC回收的對象集合)

MAT - Memory Analyzer Tool 使用進階

###3.篩選指定的Object(Hash Map,ArrayList)并按照大小進行分組

MAT - Memory Analyzer Tool 使用進階

###4.檢視指定類的Immediate dominators

MAT - Memory Analyzer Tool 使用進階

Collections fill ratio

這種方式隻能檢視那些具有預配置設定記憶體能力的集合,比如HashMap,ArrayList。計算方式:”size / capacity”

MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階

我們可以與方式一中最後一張圖所得的結果對比,一個是具體數,另一個有比例,是對應的。

##實戰三 Hash相關性能分析

當Hash集合中過多的對象傳回相同Hash值的時候,會嚴重影響性能(Hash算法原理自行搜尋),這裡來查找導緻Hash集合的碰撞率較高的罪魁禍首。

1. Map Collision Ratio

檢測每一個HashMap或者HashTable執行個體并按照碰撞率排序

碰撞率 = 碰撞的實體/Hash表中所有實體

MAT - Memory Analyzer Tool 使用進階

2. 檢視Immediate dominators

MAT - Memory Analyzer Tool 使用進階
MAT - Memory Analyzer Tool 使用進階

通過HashEntries檢視key value

MAT - Memory Analyzer Tool 使用進階

##Array等其它集合分析方法類似

##實戰四 通過OQL快速定位未使用的集合 ##

###1. 通過OQL查詢empty并且未修改過的集合:

select * from java.util.ArrayList where size=0 and modCount=0

類似的

select * from java.util.HashMap where size=0 and modCount=0

select * from java.util.Hashtable where count=0 and modCount=0

MAT - Memory Analyzer Tool 使用進階

###2. Immediate dominators(檢視引用者)

MAT - Memory Analyzer Tool 使用進階

計算空集合的Retained Size值,檢視浪費了多少記憶體

MAT - Memory Analyzer Tool 使用進階

#四 LeakCanary - 強大的記憶體洩漏分析工具

LeakCanary是square開源的記憶體洩漏排查項目,很強大,内部會幫你手動觸發GC然後分析強引用的GC引用鍊。如果存在GC引用鍊,說明有記憶體洩漏,會在你的手機上彈出個提示框,并且會自動在你手機上建立一個App。記錄了每一次記憶體洩漏的GC引用鍊,通過它可以直接定位到記憶體洩漏的未釋放的對象。原理和通過MAT分析記憶體洩漏是一樣的,隻是它完全自動化,省去了很大一部分的工作量。強烈建議內建LeakCanary。LeakCanary肯定是無法取代強大的MAT,因為它隻是隻分析記憶體洩漏,從上面的實戰中,我們可以看到,MAT的強大之處是可以對記憶體中的任何資訊進行分析。是以掌握MAT也是非常有必要的。另外在之前用的LeakCanary中發現在解析Heap Dump記憶體快照的時候會出現問題,存在小bug。

##參考文檔

MemoryAnalyzer Wiki

Android記憶體優化MAT使用入門

Android記憶體優化MAT使用進階

10-tips-for-using-the-eclipse-memory-analyzer

analyzing-java-collections-usage-with-memory-analyzer

How-to-Find-Memory-Leaks

How_to_analyze_heap_dumps

memory-for-nothing