Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 詳解Android內存洩漏檢測與MAT使用

詳解Android內存洩漏檢測與MAT使用

編輯:關於Android編程

內存洩漏基本概念

內存檢測這部分,相關的知識有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。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved