本文是使用Eclipse Memory Analyzer Tool (MAT)進(jìn)行內(nèi)存泄漏分析的筆記舍杜。遇到大大小小的內(nèi)存泄漏,都可以通過MAT分析出來桅滋。
安裝
MAT官網(wǎng) https://www.eclipse.org/mat/
To install the Memory Analyzer into an Eclipse IDE use the update site URL provided below. The Memory Analyzer (Chart) feature is optional. The chart feature requires the BIRT Chart Engine (Version 2.3.0 or greater).
總體有3種方式精拟。
- 直接下載獨立安裝包
- 下載Eclipse插件包,作為Eclipse插件安裝(Archived Update)
- 在線安裝Eclipse插件包虱歪,同樣作為插件安裝(Update Site地址)
在Mac下面,單獨安裝失敗栅表,使用Eclipse自帶插件服務(wù)器安裝失斔癖伞(太慢),改為UpdateSize的地址后怪瓶,在線安裝成功萧落。
分析
MAT的文件后綴是hprof,從AndroidStudio中得到的hprof需要經(jīng)過轉(zhuǎn)換(網(wǎng)上有教程)洗贰。這里廢話不多說找岖,直接進(jìn)入主題。
MAT界面:
- Overview(概述)
- Histogram(直方圖)
- Leak Suspects(泄漏猜測)
Overview
Leak Suspects
大部分LeakSuspects都會檢測到一些問題敛滋。下圖是一個內(nèi)存泄漏到檢測信息许布。
點擊Details ?
查看詳情,得到一個引用鏈绎晃。
這張引用鏈表示蜜唾,一張大圖片沒有被釋放 -> 被Drawable引用 -> 被CameraPopWindow引用 -> 被CBarrageView引用 -> 即CBarrageView在退出的時候沒能被釋放杂曲,出現(xiàn)了內(nèi)存泄漏。
再往下看袁余,CBarrageView沒能被釋放擎勘,是因為被一個叫HandlerAction引用,這個HandlerAction是在一個數(shù)組中颖榜,這個數(shù)組由ViewRootImpl.RunQueue持有棚饵,最后RunQueue是被Thread引用了。
查看最后的Thread掩完,選擇ListObjects - with incoming references
表示查看引用了這個Thread的對象:
可以看到噪漾,Thread作為mUiThread被DisplayActivity引用著。布局層次上藤为,DisplayActivity -> EditAndPreviewActivity(CBarrageView容器)怪与,所以,CBarrageView跨越了它的生命周期缅疟,是因為DisplayActivity里的mUiThread在持有它分别,為什么CBarrageView會被DisplayActivity的mUiThread持有呢?
此時存淫,通常有兩種方式確認(rèn)泄漏的位置耘斩。
方式1,上網(wǎng)查關(guān)鍵字桅咆。
可以看到括授,大致和View的post相關(guān)⊙冶可以查看CBarrageView里post相關(guān)是否使用得當(dāng)荚虚。
方式2,繼續(xù)跟進(jìn)源碼籍茧。
查看源碼:跟進(jìn)Activity源碼版述。
private Thread mUiThread;
mUiThread = Thread.currentThread(); // 主線程
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
猜想可能和post相關(guān),想起CBarrageView里面使用了postDelayed()寞冯,
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
return true;
}
這里渴析,ViewRootImpl,RunQueue和內(nèi)存泄漏檢測的引用鏈很接近吮龄。所以可以繼續(xù)猜想俭茧,是由于CBarrageView的postDelayed導(dǎo)致的。
跟到CBarrageView內(nèi)部漓帚,可以發(fā)現(xiàn)下面的代碼母债,Runnable并沒有主動釋放!這就是泄漏的原因:
private void initView() {
postDelayed(new Runnable() {
@Override
public void run() {
checkRowIdle();
postDelayed(this, 50);
}
}, 50);
}
這里為什么泄漏呢胰默?
- 匿名內(nèi)部類Runnable持有外部類CBarrageView的引用
- Runnable超出當(dāng)前類的生命周期(Runnable是丟在主線程的消息隊列场斑,這個是View的postDelayed接口的問題了漓踢,沒有做到自動釋放)
- 當(dāng)前類生命周期結(jié)束的時候,沒有主動釋放Runnable(CBarrageView生命周期結(jié)束漏隐,但主線程生命周期還健在)
本來假想View釋放的時候會把附帶的Runnable釋放掉喧半,但這個Runnable并不是依附到當(dāng)前View,而是主線程青责。所以不確定的地方切記使用最安全的方式挺据,否則就相當(dāng)于挖了個坑。
修改后:
private void initView() {
postDelayed(mCheckRowIdleTask, 50);
}
private Runnable mCheckRowIdleTask = new Runnable() {
@Override
public void run() {
checkRowIdle();
postDelayed(this, 50);
}
};
public void release() {
removeCallbacks(mCheckRowIdleTask);
}
Histogram
Dominator Tree
顯示大對象列表
內(nèi)存泄漏檢測與OOM定位
如果是OOM崩潰脖隶,可以直接拿到對應(yīng)hprof文件進(jìn)行分析扁耐,通常通過LeakSuspects就可以定位到OOM的位置。
內(nèi)存泄漏的檢測步驟:
2.1 確定要檢測的頁面(功能/模塊)
2.2 進(jìn)入對應(yīng)的頁面产阱,操作
2.3 退出頁面婉称,主動執(zhí)行一次GC(不執(zhí)行也行)
2.4 抓取hprof文件使用MAT進(jìn)行分析。
2.5 跳到Histogram界面构蹬,使用過濾器(圖中灰色<Regex>位置)找到對應(yīng)的頁面王暗。沒有找到,恭喜庄敛,生命周期正常俗壹,沒有內(nèi)存泄漏。找到了藻烤,則根據(jù)下面的步驟繼續(xù)定位绷雏。
MAT定位
最重要的幾個選項,優(yōu)先級從高到低:
-
Path To GC Roots
列出當(dāng)前對象到GCRoot的引用鏈(自底向上)
QQ20171127-114404@2x.png
自底向上怖亭,從當(dāng)前對象到GCRoot涎显。很容易看出,當(dāng)前對象沒能被釋放兴猩,是因為ImageView引用著棺禾,繼而被CBarrageView引用著,繼而被Thread的RunQueue引用著峭跳。
-
Merge Shortest Paths to GC Roots
列出當(dāng)前對象到GCRoot的引用鏈(自頂向下)
QQ20171127-113804@2x.png
自頂向下,從GCRoot到當(dāng)前對象缺前。如果要看CBarrageView為何沒能被釋放蛀醉,則從下往上看。如果要看當(dāng)前對象真正被引用的地方衅码,就是最底部的ImageView拯刁。換句話,對象之所以沒有被釋放逝段,是因為被ImageView引用著垛玻,被CBarrageView引用著割捅,被更上層的Thread引用著。
- With incoming references 和 With outgoing references
With incoming references 是列出引用當(dāng)前對象的對象帚桩。
With outgoing references 是列出當(dāng)前對象引用的對象亿驾。
如果當(dāng)前對象有泄漏,則只需看 incoming references即可账嚎,即找出它被什么對象引用了導(dǎo)致了生命周期異常莫瞬。根本無需關(guān)心它引用了哪些對象!郭蕉!
在內(nèi)存泄漏方面疼邀,outgoing references并沒有什么用,切記不要點召锈,只會混淆視聽
- List Objects 和 Show Objects By Class
List Objects是按照類的實例(對象)來顯示旁振。
Show Objects By Class是按照類名來顯示。
差異自行感知涨岁。
- references的過濾器
5.1 with all references 即所有引用都顯示出來
5.2 exclude weak reference 即不顯示弱引用
5.3 exclude soft reference 即不顯示軟引用
5.4 exclude phantom reference 即不顯示幽靈引用
由于weak/soft/phantom引用都可以被GC回收拐袜,所以三者都可以不顯示。通常使用exclude weak/soft reference卵惦。(weak是引用可回收時立即被回收變?yōu)閚ull阻肿,soft是引用可回收但會等到內(nèi)存不足時才回收,phantom是用來跟蹤引用釋放用的沮尿,本身不會產(chǎn)生強(qiáng)引用)
回到實例
如上面所述丛塌,懷疑CBarrageView有內(nèi)存泄漏,在退出了CBarrageView后捕獲hprof文件畜疾。
選擇Histogram(Dominator Tree可以忽略了赴邻,后面會說)
-
按照包名過濾:com.xxx.xxx
QQ20171127-153205@2x.png 如圖,選擇含有CBarrageView(如CBarrageView$CRecycleBin)啡捶,或者在CBarrageView里引用的對象(如CBarrageItem)都可以姥敛。因為CBarrageView沒有釋放,其內(nèi)部引用的對象也不會釋放瞎暑,最后到GCRoot的引用鏈?zhǔn)且恢碌耐病_@里要注意不要使用對象數(shù)為0的來分析,因為這種無法生成引用鏈了赌。同時墨榄,建議使用更底層的對象,因為當(dāng)前對象不能釋放很可能是由于內(nèi)部b(見另一個案例)勿她。
-
右鍵,選擇Merge Shortest Paths to GC Roots,過濾掉weak和soft引用之剧,然后就生成下面的引用鏈(自頂向下)郭卫。
QQ20171127-154629@2x.png
注:自頂向下,黑色部分是變量背稼,變量的類型是上一條贰军。如圖,localValues變量的類型并不是Values雇庙,而是上面的Thread谓形;mActions變量并不是ArrayList而是上面那條RunQueue。
- 如果習(xí)慣自底向上分析疆前,可以按照步驟3再選擇一個對象寒跳,右鍵選擇Path To GC Roots,同樣過濾掉weak和soft引用竹椒,然后就生成下面的引用鏈(自底向上)童太。
QQ20171127-154647@2x.png
注:自底向上,黑體部分是變量胸完,變量的類型緊隨其后书释,和自頂向下不同!如圖赊窥,mActions變量類型是RunQueue爆惧,是下面table變量的一個元素。
過濾掉weak和soft引用可以減少不必要的分析锨能,因為誤分析了weak和soft的引用其實一點幫助都沒有扯再,只會浪費時間。
根據(jù)引用鏈猜想或定位問題址遇∠ㄗ瑁可以通過源碼,或者通過搜索關(guān)鍵字倔约。
最后秃殉,回答為什么不使用Dominator Tree。因為這個界面并不能百分百生成引用鏈浸剩。如下圖钾军,CBarrageView相關(guān)的對象,幾乎有一半沒能正確生成引用鏈绢要。這會誤導(dǎo)巧颈,模塊沒有發(fā)生內(nèi)存泄漏,所以不要再使用Dominator Tree界面來進(jìn)行內(nèi)存泄漏分析袖扛。這個界面就只是單純看大對象就算了!!
一些內(nèi)存泄漏例子
android.view.ViewRootImpl$RunQueue
使用了View的post或postDelayed沒有進(jìn)行Runnable的主動釋放蛆封。導(dǎo)致Runnable泄漏到主線程唇礁。android.app.LoadedApk$ReceiverDispatcher$InnerReceiver
mDispatcher java.lang.ref.WeakReference
mContext android.app.LoadedApk$ReceiverDispatcher
使用了廣播,注冊了沒有進(jìn)行反注冊惨篱,導(dǎo)致泄漏到廣播分發(fā)隊列中盏筐。