原文鏈接:https://www.jb51.net/article/100837.htm
內存泄漏基本概念
內存檢測這部分,相關的知識有JVM虛擬機垃圾收集機制,類加載機制,內存模型等。編寫沒有內存泄漏的程序,對提高程序穩定性,提高用戶體驗具有重要的意義。因此,學習Java利用java編寫程序的時候,要特別注意內存泄漏相關的問題。雖然JVM提供了自動垃圾回收機制,但是還是有很多情況會導致內存泄漏。?
內存泄漏主要原因就是一個生命周期長的對象,持有了一個生命周期短的對象的引用。這樣,會導致短的對象在該回收時候無法被回收。Android中比較典型的有:1、靜態變量持有Activity的context。2、或者Handler持有某個組件的context,同時如果Looper的消息隊列中有針對該Handler的消息沒有被處理,那么會被作為target持有強引用,最終的導致context無法釋放,導致相應組件在退出時無法被內存回收。3、非靜態內部類默認持有外部類的引用,這樣如果我們在Activity中定義了一個Thread內部類,同時直接通過new Thread的方式去運行線程,那么在線程運行結束之前,線程都會持有Activity的引用,從而導致Activity無法被釋放。
內存檢測工具
LeakCananry
LeakCanary,主要監測的是使用過程中Activity,Fragment等組件是否沒被內存回收。使用方法也十分簡單,相當于裝了一個監聽器,然后通過正常 操作去尋找內存泄漏,發生內存泄漏的時候會有Toast,同時可以在相應程序查看哪里發生內存泄漏。?
方法比較簡單,添加leakcanary依賴以后,新建一個Application入口,在Oncreate方法中安裝Leakcanary即可。
當發生內存泄漏時,屏幕會出現Toast,同時打開桌面上的Leaks程序,顯示泄漏的內存,如下圖:?
LeakCananry實現步驟大致是:?
實現大致步驟是:?
1、自動把activity加入到KeyedWeakReference?
2、在background線程中,檢查onDestroy后reference是否被清除,且沒有觸發gc?
3、如果reference沒有被清除,則dump heap到一個hprof文件并保存到app文件系統中?
4、在一個單獨進程中啟動HeapAnalyzerService,HeapAnalyzer使用HAHA來分析heap dump。?
5、HeapAnalyzer在heap dump中根據reference key找到KeyedWeakReference。?
6、HeapAnalyzer計算出到GC Roots的最短強引用路徑來判斷是否存在泄露,然后build出造成這個泄露的引用鏈。?
7、結果被傳回來app進程的DisplayLeakService,并展示一個泄露的notification。?
方法的有點是簡單易行,但是只能檢測Activity、Fragment是否發生內存泄漏。
觀看整體內存使用情況
詳情參見官方文檔:?https://developer.android.com/studio/profile/investigate-ram.html#ViewingAllocations?
使用adb shell,進入手機adb,執行命令:
dumpsys meminfo <包名> [-參數]
可以查看應用不同部分內存分配情況。比如Java heap,Native heap等?
輸出是目前具體應用的內存分配,單位是kilobytes?
因為程序涉及jni,經常會分配本地內存,所以會使用adb shell 的方式去查看native heap的分配情況。
結果如下:
分析各個參數:?
Private Clean/Dirty RAM:?
這部分內存是app的私有內存,當app銷毀是操作系統可以回收到的內存。其中private dirty只能被你的進程使用,同時只能存在在內存當中,當內存不夠,也不能通過分頁技術存儲到硬盤(操作系統相關知識),dalvik和native heap上的分配都是private dirty RAM。因為是dalvik heap和native heap共享的內存,所以命名dirty?
DDMS
使用流程
啟動eclipse后,切換到DDMS透視圖,并確認Devices視圖、Heap視圖都是打開的;
將手機通過USB鏈接至電腦,鏈接時需要確認手機是處于“USB調試”模式,而不是作為“MassStorage”;
鏈接成功后,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;
點擊選中想要監測的進程,比如system_process進程;
點擊選中Devices視圖界面中最上方一排圖標中的“Update Heap”圖標;
點擊Heap視圖中的“Cause GC”按鈕;
此時在Heap視圖中就會看到當前選中的進程的內存使用量的詳細情況。
如何檢測內存泄漏?
Heap視圖中部有一個Type叫做dataobject,即數據對象,也就是我們的程序中實例化的對象。在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。?
正常情況下Total Size值都會穩定在一個有限的范圍內,也就是說沒有造成對象不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存占用量會會落到一個穩定的水平。如果代碼中存在沒有釋放對象引用的情況,則dataobject的Total Size值在每次GC后不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大
通過DDMS方式,DataObject 的totalSize如果穩定在一個大概范圍內,則可以確定沒有發生內存泄漏。
MAT
然而,并不是所有的內存泄漏都十分明顯,并且會最終導致OOM。有時候只有幾個對象被泄漏,雖然影響不大,但是無疑浪費了內存。?
要發現這種比較隱蔽的內存泄漏,我們需要使用MAT工具。?
在了解支配樹之前,要先了解一些相關概念。
支配樹
支配樹體現了對象實例間的支配關系,在對象引用圖中,所有指向對象B的路徑都經過對象A,則認為對象A支配對象B。?
在這張圖里,左邊是對象引用關系,對于A和B,要抵達這兩個點必須經過GC root。而對于C可以從A也可以從B抵達,但都必須經過GC root,所以最近的支配點同樣也是GC root。?
對于點D,不管是從C->D還是C->D->F->D,都必須經過的最近的點是C,所以C是D的支配點。同理可得EFHG在支配樹中的位置。
SHALLOWHEAP和RETAINED HEAP
Shallow heap表示對象本身所占內存大小,一個內存大小100bytes的對象Shallow heap就是100bytes。?
Retained heap表示通過回收這一個對象總共能回收的內存,比方說一個100bytes的對象還直接或者間接地持有了另外3個100bytes的對象引用,回收這個對象的時候如果另外3個對象沒有其他引用也能被回收掉的時候,Retained heap就是400bytes。?
在使用mat進行分析時,我們常常接觸到的數據就是shallow size和retained size: Shallow Size?
對象自身占用的內存大小,不包括它引用的對象。?
針對非數組類型的對象,它的大小就是對象與它所有的成員變量大小的總和。當然這里面還會包括一些java語言特性的數據存儲單元。?
針對數組類型的對象,它的大小是數組元素對象的大小總和。?
Retained Size?
Retained Size=當前對象大小+當前對象可直接或間接引用到的對象的大小總和。(間接引用的含義:A->B->C, C就是間接引用)?
換句話說,Retained Size就是當前對象被GC后,從Heap上總共能釋放掉的內存。?
不過,釋放的時候還要排除被GC Roots直接或間接引用的對象。他們暫時不會被回收。如下圖:?
A對象的Retained Size=A對象的Shallow Size?
B對象的Retained Size=B對象的Shallow Size + C對象的Shallow Size?
因為B對象被釋放時,C同時被釋放,而D由于被GC roots直接引用所以不會被釋放。而Retained Size就是當前對象被GC后,從Heap上總共能釋放掉的內存。
以上概念,都是在使用MAT進行內存分析經常使用的,所以要記住。
MAT的下載與使用
下載地址:https://eclipse.org/mat/downloads.php?
這里沒有作為eclipse插件的方式下載mat,而是通過下載單獨的軟件客戶端。?
首先,在DDMS中選擇要檢測的進程并dump HPROF file,如下圖:?
HPROF中存儲的是當前內存的快照,因此,在dump快照之前先點擊cause GC手動觸發一次垃圾回收,這樣可以避免軟引用、弱引用等不必要的對象保留在內存中影響我們的分析。
轉儲出來的hprof文件,還有使用sdk自帶工具進行一下格式轉化,工具在sdk路徑下的platform-tools下,名稱為hprof-conv。
使用方法:?
/.hprof-conv.exe a.hprof b.hprof?
a 是輸入hprof文件名,b是輸出文件名。?
然后將b.hprof在eclipse memory Analyzer中打開,注意要轉換格式,不然無法成功打開。?
如下:
利用MAT分析內存泄漏
分析過程中,主要使用的是Histogram直方圖,和Dominater tree支配樹。
在Histogram視圖中查找retained heap值最大的項,并分析這里是否發生內存泄漏。
注意,一般情況下我們忽略java、android系統自帶的對象,而著重分析我們自己程序中的對象。所以在上面輸入過濾Class Name。
Retained heap表示因為這個對象,會導致多少對象無法回收。
右擊相應類,list objects->with incoming references。表明引用這個類的某個實例的其它類,也就是它在引用樹中的父節點。通過分析該對象被誰引用,來判斷為何沒被垃圾回收。?
outcoming reference就是子節點,查看一些當前對象引用著的對象。
此外看,Merge shortest path to gc root,可以找到一條到GC root的最短路徑,來看為什么當前對象無法被回收。
實戰分析
下面記錄了本人對一個項目的具體分析過程,以及各個工具的使用方法。
1、使用DDMS查看內存
使用DDMS的過程中,針對應用分別進行了多次檢測,主要查看程序運行前的內存使用情況和程序運行后的內存使用情況:?
使用前:
使用后:?
通過上述數據可以看到,在程序運行前data object也就是在堆上分配的數據是180KB左右,而運行后內存大概在300KB上下浮動,沒有呈現一個明顯的一直上升的情況,故而沒有明顯的內存泄漏,基本沒有導致OOM的可能。
但是,可以發現,程序運行一次以后,放置一段時間,即便手動觸發GC,堆上的內存雖然回落,但是仍然是288KB,與執行前的180KB相差較大,說明有一些對象被GC roots引用,無法完成釋放。
下面采用MAT工具進行進一步分析。在上面的過程中,轉出了三個hprof文件,將hprof文件利用Android sdk tools下的工具進行格式轉換,進行對比分析:
2、使用MAT分析內存轉儲
前面分析內存使用發現,使用前和使用后有一個100KB左右的差值,同時即便放置一段時間仍然無法使用。將before和after的直方圖加入對比欄,在MAT中進行對比:
點擊右上角的紅色嘆號:
對比發現兩個shallow heap大小基本相同,多出的部分是UpdatePartResultThread,系統類而不是我們自己編寫程序造成的。?
再看一下使用前后直方圖中的retained heap:
可以看出,程序執行后,newActivity強引用了一些對象,在newAcitivity沒有推出前,retainedheap部分內存無法被回收。這也就是我們在DDMS中發現堆內存差異的主要原因。?
右擊直方圖中的NewActivity,可以看見如下選項:
用的比較多的是List objects和Merger shortest Paths to GC Roots。?
List objects:?
Outgoing reference是支配樹中當前對象的子節點,也就是當前對象持有哪些引用。?
Incoming reference是父節點,即當前對象被誰引用,為什么沒被回收。
Merger shortest Paths to GC Roots:找到當前無法被釋放的對象到GC roots的最短路徑。即排查當前對象被誰引用,為什么沒有被釋放。這里因為我們的對象是一個Activity,當它顯示在前臺的時候,不會被垃圾回收,所以不是我們分析的點。
在這里,我們查看outgoing reference,查看當前對象擁有哪些強引用:
排除系統的對象,還是主要分析我們編寫的程序。
最后發現,我們在之前使用LeakCanary時,注冊的相應監聽器沒有回收,發現了內存泄漏 :)。
去掉LeakCanary,再次測試發現data object的值確實下降了不少。
繼續分析,發現newActivity引用了一個
致使一部分內存無法被釋放。這個問題屬于客戶端實現問題,不在內存泄漏的范圍內。?
接下來,在直方圖中過濾出服務端的類:
?
可以看到,服務端的類大部分shallow heap都為0,也就是已經被垃圾回收。
結論
在使用MAT分析內存時,最關鍵的就是找引用關系。如果一個應該被釋放的對象沒有被釋放,那么我們往往要查看它的incoming reference,看看是誰持有了它的強引用。同時利用Merger shortest GC roots找到到GC root的最短路徑,確定是由于被誰引用而導致無法GC。