1. 程式人生 > >【轉】如何使用MAT分析內存泄漏

【轉】如何使用MAT分析內存泄漏

提示框 體驗 get() 關於 row ras 內存問題 nta cit

原文鏈接:http://www.lightskystreet.com/2015/09/01/mat_usage/

MAT - Memory Analyzer Tool 使用進階

Sep 1, 2015 | Android性能優化 | Hits

#前言#
盡管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

技術分享圖片

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。

技術分享圖片

2.5 Path to GC Roots

查看一個對象到RC Roots的引用鏈
通常在排查內存泄漏的時候,我們會選擇exclude all phantom/weak/soft etc.references,
意思是查看排除虛引用/弱引用/軟引用等的引用鏈,因為被虛引用/弱引用/軟引用的對象可以直接被GC給回收,我們要看的就是某個對象否還存在Strong 引用鏈(在導出HeapDump之前要手動出發GC來保證),如果有,則說明存在內存泄漏,然後再去排查具體引用。

技術分享圖片

###查看當前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

直接進行比較
技術分享圖片

技術分享圖片

技術分享圖片

方式二:Compare Baseket

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

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

引出一個同事開發過程中的一個真實的例子,通過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分析還是需要一些經驗,能夠幫你更快更準確的定位。

##實戰二 集合使用率分析
集合在開發中會經常使用到,如何選擇合適的數據結構的集合,初始容量是多少(太小,可能導致頻繁擴容),太大,又會開銷跟多內存。當這些問題不是很明確時或者想查看集合的使用情況時,可以通過MAT來進行分析。

###1.篩選目標對象
技術分享圖片

###2.Show Retained Set(查找當X被回收時那些將被GC回收的對象集合)
技術分享圖片

###3.篩選指定的Object(Hash Map,ArrayList)並按照大小進行分組
技術分享圖片

###4.查看指定類的Immediate dominators
技術分享圖片

Collections fill ratio

這種方式只能查看那些具有預分配內存能力的集合,比如HashMap,ArrayList。計算方式:”size / capacity”

技術分享圖片

技術分享圖片

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

##實戰三 Hash相關性能分析
當Hash集合中過多的對象返回相同Hash值的時候,會嚴重影響性能(Hash算法原理自行搜索),這裏來查找導致Hash集合的碰撞率較高的罪魁禍首。

1. Map Collision Ratio

檢測每一個HashMap或者HashTable實例並按照碰撞率排序
碰撞率 = 碰撞的實體/Hash表中所有實體

技術分享圖片

2. 查看Immediate dominators

技術分享圖片
技術分享圖片

通過HashEntries查看key value

技術分享圖片

##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

技術分享圖片

###2. Immediate dominators(查看引用者)
技術分享圖片

計算空集合的Retained Size值,查看浪費了多少內存

技術分享圖片

#四 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

【轉】如何使用MAT分析內存泄漏