(轉(zhuǎn)載)詳解Android內(nèi)存泄漏檢測(cè)與MAT使用

原文鏈接:https://www.jb51.net/article/100837.htm

內(nèi)存泄漏基本概念

內(nèi)存檢測(cè)這部分,相關(guān)的知識(shí)有JVM虛擬機(jī)垃圾收集機(jī)制,類加載機(jī)制,內(nèi)存模型等。編寫沒有內(nèi)存泄漏的程序,對(duì)提高程序穩(wěn)定性,提高用戶體驗(yàn)具有重要的意義。因此,學(xué)習(xí)Java利用java編寫程序的時(shí)候,要特別注意內(nèi)存泄漏相關(guān)的問(wèn)題。雖然JVM提供了自動(dòng)垃圾回收機(jī)制,但是還是有很多情況會(huì)導(dǎo)致內(nèi)存泄漏。?

內(nèi)存泄漏主要原因就是一個(gè)生命周期長(zhǎng)的對(duì)象,持有了一個(gè)生命周期短的對(duì)象的引用。這樣,會(huì)導(dǎo)致短的對(duì)象在該回收時(shí)候無(wú)法被回收。Android中比較典型的有:1、靜態(tài)變量持有Activity的context。2、或者Handler持有某個(gè)組件的context,同時(shí)如果Looper的消息隊(duì)列中有針對(duì)該Handler的消息沒有被處理,那么會(huì)被作為target持有強(qiáng)引用,最終的導(dǎo)致context無(wú)法釋放,導(dǎo)致相應(yīng)組件在退出時(shí)無(wú)法被內(nèi)存回收。3、非靜態(tài)內(nèi)部類默認(rèn)持有外部類的引用,這樣如果我們?cè)贏ctivity中定義了一個(gè)Thread內(nèi)部類,同時(shí)直接通過(guò)new Thread的方式去運(yùn)行線程,那么在線程運(yùn)行結(jié)束之前,線程都會(huì)持有Activity的引用,從而導(dǎo)致Activity無(wú)法被釋放。

內(nèi)存檢測(cè)工具

LeakCananry

LeakCanary,主要監(jiān)測(cè)的是使用過(guò)程中Activity,F(xiàn)ragment等組件是否沒被內(nèi)存回收。使用方法也十分簡(jiǎn)單,相當(dāng)于裝了一個(gè)監(jiān)聽器,然后通過(guò)正常 操作去尋找內(nèi)存泄漏,發(fā)生內(nèi)存泄漏的時(shí)候會(huì)有Toast,同時(shí)可以在相應(yīng)程序查看哪里發(fā)生內(nèi)存泄漏。?

方法比較簡(jiǎn)單,添加leakcanary依賴以后,新建一個(gè)Application入口,在Oncreate方法中安裝Leakcanary即可。

當(dāng)發(fā)生內(nèi)存泄漏時(shí),屏幕會(huì)出現(xiàn)Toast,同時(shí)打開桌面上的Leaks程序,顯示泄漏的內(nèi)存,如下圖:?

LeakCananry實(shí)現(xiàn)步驟大致是:?

實(shí)現(xiàn)大致步驟是:?

1、自動(dòng)把a(bǔ)ctivity加入到KeyedWeakReference?

2、在background線程中,檢查onDestroy后reference是否被清除,且沒有觸發(fā)gc?

3、如果reference沒有被清除,則dump heap到一個(gè)hprof文件并保存到app文件系統(tǒng)中?

4、在一個(gè)單獨(dú)進(jìn)程中啟動(dòng)HeapAnalyzerService,HeapAnalyzer使用HAHA來(lái)分析heap dump。?

5、HeapAnalyzer在heap dump中根據(jù)reference key找到KeyedWeakReference。?

6、HeapAnalyzer計(jì)算出到GC Roots的最短強(qiáng)引用路徑來(lái)判斷是否存在泄露,然后build出造成這個(gè)泄露的引用鏈。?

7、結(jié)果被傳回來(lái)app進(jìn)程的DisplayLeakService,并展示一個(gè)泄露的notification。?

方法的有點(diǎn)是簡(jiǎn)單易行,但是只能檢測(cè)Activity、Fragment是否發(fā)生內(nèi)存泄漏。

觀看整體內(nèi)存使用情況

詳情參見官方文檔:?https://developer.android.com/studio/profile/investigate-ram.html#ViewingAllocations?

使用adb shell,進(jìn)入手機(jī)adb,執(zhí)行命令:

dumpsys meminfo <包名> [-參數(shù)]

可以查看應(yīng)用不同部分內(nèi)存分配情況。比如Java heap,Native heap等?

輸出是目前具體應(yīng)用的內(nèi)存分配,單位是kilobytes?

因?yàn)槌绦蛏婕癹ni,經(jīng)常會(huì)分配本地內(nèi)存,所以會(huì)使用adb shell 的方式去查看native heap的分配情況。

結(jié)果如下:

分析各個(gè)參數(shù):?

Private Clean/Dirty RAM:?

這部分內(nèi)存是app的私有內(nèi)存,當(dāng)app銷毀是操作系統(tǒng)可以回收到的內(nèi)存。其中private dirty只能被你的進(jìn)程使用,同時(shí)只能存在在內(nèi)存當(dāng)中,當(dāng)內(nèi)存不夠,也不能通過(guò)分頁(yè)技術(shù)存儲(chǔ)到硬盤(操作系統(tǒng)相關(guān)知識(shí)),dalvik和native heap上的分配都是private dirty RAM。因?yàn)槭莇alvik heap和native heap共享的內(nèi)存,所以命名dirty?

DDMS

使用流程

啟動(dòng)eclipse后,切換到DDMS透視圖,并確認(rèn)Devices視圖、Heap視圖都是打開的;

將手機(jī)通過(guò)USB鏈接至電腦,鏈接時(shí)需要確認(rèn)手機(jī)是處于“USB調(diào)試”模式,而不是作為“MassStorage”;

鏈接成功后,在DDMS的Devices視圖中將會(huì)顯示手機(jī)設(shè)備的序列號(hào),以及設(shè)備中正在運(yùn)行的部分進(jìn)程信息;

點(diǎn)擊選中想要監(jiān)測(cè)的進(jìn)程,比如system_process進(jìn)程;

點(diǎn)擊選中Devices視圖界面中最上方一排圖標(biāo)中的“Update Heap”圖標(biāo);

點(diǎn)擊Heap視圖中的“Cause GC”按鈕;

此時(shí)在Heap視圖中就會(huì)看到當(dāng)前選中的進(jìn)程的內(nèi)存使用量的詳細(xì)情況。

如何檢測(cè)內(nèi)存泄漏?

Heap視圖中部有一個(gè)Type叫做dataobject,即數(shù)據(jù)對(duì)象,也就是我們的程序中實(shí)例化的對(duì)象。在data object一行中有一列是“Total Size”,其值就是當(dāng)前進(jìn)程中所有Java數(shù)據(jù)對(duì)象的內(nèi)存總量,一般情況下,這個(gè)值的大小決定了是否會(huì)有內(nèi)存泄漏。?

正常情況下Total Size值都會(huì)穩(wěn)定在一個(gè)有限的范圍內(nèi),也就是說(shuō)沒有造成對(duì)象不被垃圾回收的情況,所以說(shuō)雖然我們不斷的操作會(huì)不斷的生成很多對(duì)象,而在虛擬機(jī)不斷的進(jìn)行GC的過(guò)程中,這些對(duì)象都被回收了,內(nèi)存占用量會(huì)會(huì)落到一個(gè)穩(wěn)定的水平。如果代碼中存在沒有釋放對(duì)象引用的情況,則dataobject的Total Size值在每次GC后不會(huì)有明顯的回落,隨著操作次數(shù)的增多Total Size的值會(huì)越來(lái)越大

通過(guò)DDMS方式,DataObject 的totalSize如果穩(wěn)定在一個(gè)大概范圍內(nèi),則可以確定沒有發(fā)生內(nèi)存泄漏。

MAT

然而,并不是所有的內(nèi)存泄漏都十分明顯,并且會(huì)最終導(dǎo)致OOM。有時(shí)候只有幾個(gè)對(duì)象被泄漏,雖然影響不大,但是無(wú)疑浪費(fèi)了內(nèi)存。?

要發(fā)現(xiàn)這種比較隱蔽的內(nèi)存泄漏,我們需要使用MAT工具。?

在了解支配樹之前,要先了解一些相關(guān)概念。

支配樹

支配樹體現(xiàn)了對(duì)象實(shí)例間的支配關(guān)系,在對(duì)象引用圖中,所有指向?qū)ο驜的路徑都經(jīng)過(guò)對(duì)象A,則認(rèn)為對(duì)象A支配對(duì)象B。?


在這張圖里,左邊是對(duì)象引用關(guān)系,對(duì)于A和B,要抵達(dá)這兩個(gè)點(diǎn)必須經(jīng)過(guò)GC root。而對(duì)于C可以從A也可以從B抵達(dá),但都必須經(jīng)過(guò)GC root,所以最近的支配點(diǎn)同樣也是GC root。?

對(duì)于點(diǎn)D,不管是從C->D還是C->D->F->D,都必須經(jīng)過(guò)的最近的點(diǎn)是C,所以C是D的支配點(diǎn)。同理可得EFHG在支配樹中的位置。

SHALLOWHEAP和RETAINED HEAP

Shallow heap表示對(duì)象本身所占內(nèi)存大小,一個(gè)內(nèi)存大小100bytes的對(duì)象Shallow heap就是100bytes。?

Retained heap表示通過(guò)回收這一個(gè)對(duì)象總共能回收的內(nèi)存,比方說(shuō)一個(gè)100bytes的對(duì)象還直接或者間接地持有了另外3個(gè)100bytes的對(duì)象引用,回收這個(gè)對(duì)象的時(shí)候如果另外3個(gè)對(duì)象沒有其他引用也能被回收掉的時(shí)候,Retained heap就是400bytes。?

在使用mat進(jìn)行分析時(shí),我們常常接觸到的數(shù)據(jù)就是shallow size和retained size: Shallow Size?

對(duì)象自身占用的內(nèi)存大小,不包括它引用的對(duì)象。?

針對(duì)非數(shù)組類型的對(duì)象,它的大小就是對(duì)象與它所有的成員變量大小的總和。當(dāng)然這里面還會(huì)包括一些java語(yǔ)言特性的數(shù)據(jù)存儲(chǔ)單元。?

針對(duì)數(shù)組類型的對(duì)象,它的大小是數(shù)組元素對(duì)象的大小總和。?

Retained Size?

Retained Size=當(dāng)前對(duì)象大小+當(dāng)前對(duì)象可直接或間接引用到的對(duì)象的大小總和。(間接引用的含義:A->B->C, C就是間接引用)?

換句話說(shuō),Retained Size就是當(dāng)前對(duì)象被GC后,從Heap上總共能釋放掉的內(nèi)存。?

不過(guò),釋放的時(shí)候還要排除被GC Roots直接或間接引用的對(duì)象。他們暫時(shí)不會(huì)被回收。如下圖:?

A對(duì)象的Retained Size=A對(duì)象的Shallow Size?

B對(duì)象的Retained Size=B對(duì)象的Shallow Size + C對(duì)象的Shallow Size?

因?yàn)锽對(duì)象被釋放時(shí),C同時(shí)被釋放,而D由于被GC roots直接引用所以不會(huì)被釋放。而Retained Size就是當(dāng)前對(duì)象被GC后,從Heap上總共能釋放掉的內(nèi)存。

以上概念,都是在使用MAT進(jìn)行內(nèi)存分析經(jīng)常使用的,所以要記住。

MAT的下載與使用

下載地址:https://eclipse.org/mat/downloads.php?

這里沒有作為eclipse插件的方式下載mat,而是通過(guò)下載單獨(dú)的軟件客戶端。?

首先,在DDMS中選擇要檢測(cè)的進(jìn)程并dump HPROF file,如下圖:?

HPROF中存儲(chǔ)的是當(dāng)前內(nèi)存的快照,因此,在dump快照之前先點(diǎn)擊cause GC手動(dòng)觸發(fā)一次垃圾回收,這樣可以避免軟引用、弱引用等不必要的對(duì)象保留在內(nèi)存中影響我們的分析。

轉(zhuǎn)儲(chǔ)出來(lái)的hprof文件,還有使用sdk自帶工具進(jìn)行一下格式轉(zhuǎn)化,工具在sdk路徑下的platform-tools下,名稱為hprof-conv。

使用方法:?

/.hprof-conv.exe a.hprof b.hprof?

a 是輸入hprof文件名,b是輸出文件名。?

然后將b.hprof在eclipse memory Analyzer中打開,注意要轉(zhuǎn)換格式,不然無(wú)法成功打開。?

如下:

利用MAT分析內(nèi)存泄漏

分析過(guò)程中,主要使用的是Histogram直方圖,和Dominater tree支配樹。

在Histogram視圖中查找retained heap值最大的項(xiàng),并分析這里是否發(fā)生內(nèi)存泄漏。

注意,一般情況下我們忽略java、android系統(tǒng)自帶的對(duì)象,而著重分析我們自己程序中的對(duì)象。所以在上面輸入過(guò)濾Class Name。

Retained heap表示因?yàn)檫@個(gè)對(duì)象,會(huì)導(dǎo)致多少對(duì)象無(wú)法回收。

右擊相應(yīng)類,list objects->with incoming references。表明引用這個(gè)類的某個(gè)實(shí)例的其它類,也就是它在引用樹中的父節(jié)點(diǎn)。通過(guò)分析該對(duì)象被誰(shuí)引用,來(lái)判斷為何沒被垃圾回收。?

outcoming reference就是子節(jié)點(diǎn),查看一些當(dāng)前對(duì)象引用著的對(duì)象。

此外看,Merge shortest path to gc root,可以找到一條到GC root的最短路徑,來(lái)看為什么當(dāng)前對(duì)象無(wú)法被回收。

實(shí)戰(zhàn)分析

下面記錄了本人對(duì)一個(gè)項(xiàng)目的具體分析過(guò)程,以及各個(gè)工具的使用方法。

1、使用DDMS查看內(nèi)存

使用DDMS的過(guò)程中,針對(duì)應(yīng)用分別進(jìn)行了多次檢測(cè),主要查看程序運(yùn)行前的內(nèi)存使用情況和程序運(yùn)行后的內(nèi)存使用情況:?

使用前:

使用后:?

通過(guò)上述數(shù)據(jù)可以看到,在程序運(yùn)行前data object也就是在堆上分配的數(shù)據(jù)是180KB左右,而運(yùn)行后內(nèi)存大概在300KB上下浮動(dòng),沒有呈現(xiàn)一個(gè)明顯的一直上升的情況,故而沒有明顯的內(nèi)存泄漏,基本沒有導(dǎo)致OOM的可能。

但是,可以發(fā)現(xiàn),程序運(yùn)行一次以后,放置一段時(shí)間,即便手動(dòng)觸發(fā)GC,堆上的內(nèi)存雖然回落,但是仍然是288KB,與執(zhí)行前的180KB相差較大,說(shuō)明有一些對(duì)象被GC roots引用,無(wú)法完成釋放。

下面采用MAT工具進(jìn)行進(jìn)一步分析。在上面的過(guò)程中,轉(zhuǎn)出了三個(gè)hprof文件,將hprof文件利用Android sdk tools下的工具進(jìn)行格式轉(zhuǎn)換,進(jìn)行對(duì)比分析:

2、使用MAT分析內(nèi)存轉(zhuǎn)儲(chǔ)

前面分析內(nèi)存使用發(fā)現(xiàn),使用前和使用后有一個(gè)100KB左右的差值,同時(shí)即便放置一段時(shí)間仍然無(wú)法使用。將before和after的直方圖加入對(duì)比欄,在MAT中進(jìn)行對(duì)比:

點(diǎn)擊右上角的紅色嘆號(hào):

對(duì)比發(fā)現(xiàn)兩個(gè)shallow heap大小基本相同,多出的部分是UpdatePartResultThread,系統(tǒng)類而不是我們自己編寫程序造成的。?

再看一下使用前后直方圖中的retained heap:

可以看出,程序執(zhí)行后,newActivity強(qiáng)引用了一些對(duì)象,在newAcitivity沒有推出前,retainedheap部分內(nèi)存無(wú)法被回收。這也就是我們?cè)贒DMS中發(fā)現(xiàn)堆內(nèi)存差異的主要原因。?

右擊直方圖中的NewActivity,可以看見如下選項(xiàng):

用的比較多的是List objects和Merger shortest Paths to GC Roots。?

List objects:?

Outgoing reference是支配樹中當(dāng)前對(duì)象的子節(jié)點(diǎn),也就是當(dāng)前對(duì)象持有哪些引用。?

Incoming reference是父節(jié)點(diǎn),即當(dāng)前對(duì)象被誰(shuí)引用,為什么沒被回收。

Merger shortest Paths to GC Roots:找到當(dāng)前無(wú)法被釋放的對(duì)象到GC roots的最短路徑。即排查當(dāng)前對(duì)象被誰(shuí)引用,為什么沒有被釋放。這里因?yàn)槲覀兊膶?duì)象是一個(gè)Activity,當(dāng)它顯示在前臺(tái)的時(shí)候,不會(huì)被垃圾回收,所以不是我們分析的點(diǎn)。

在這里,我們查看outgoing reference,查看當(dāng)前對(duì)象擁有哪些強(qiáng)引用:

排除系統(tǒng)的對(duì)象,還是主要分析我們編寫的程序。

最后發(fā)現(xiàn),我們?cè)谥笆褂肔eakCanary時(shí),注冊(cè)的相應(yīng)監(jiān)聽器沒有回收,發(fā)現(xiàn)了內(nèi)存泄漏 :)。

去掉LeakCanary,再次測(cè)試發(fā)現(xiàn)data object的值確實(shí)下降了不少。

繼續(xù)分析,發(fā)現(xiàn)newActivity引用了一個(gè)

致使一部分內(nèi)存無(wú)法被釋放。這個(gè)問(wèn)題屬于客戶端實(shí)現(xiàn)問(wèn)題,不在內(nèi)存泄漏的范圍內(nèi)。?

接下來(lái),在直方圖中過(guò)濾出服務(wù)端的類:

?

可以看到,服務(wù)端的類大部分shallow heap都為0,也就是已經(jīng)被垃圾回收。

結(jié)論

在使用MAT分析內(nèi)存時(shí),最關(guān)鍵的就是找引用關(guān)系。如果一個(gè)應(yīng)該被釋放的對(duì)象沒有被釋放,那么我們往往要查看它的incoming reference,看看是誰(shuí)持有了它的強(qiáng)引用。同時(shí)利用Merger shortest GC roots找到到GC root的最短路徑,確定是由于被誰(shuí)引用而導(dǎo)致無(wú)法GC。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容