Android 内存检测工具
所謂內存泄漏,是指本該被回收的內存由于某種原因繞開了GC回收算法,從而導致該內存無法被有效數據使用而使得總內存減小的情況。
內存泄漏會導致內存消耗的增加,大量的消耗會使得APP OOM,特別是在一些內存比較小的機器上。下面我們看看有哪些工具可以用來分析內存泄漏。
Heap Dump
Heap Dump的主要功能就是查看不同的數據類型在內存中的使用情況。它可以幫助你找到大對象,也可以通過數據的變化發現內存泄漏。
使用Heap Dump
打開Android Device Monitor工具,在左邊Devices列表中選擇要查看的應用程序進程,點擊Update Heap按鈕,在右邊選擇Heap選項,并點擊Cause GC按鈕,就會開始顯示數據。我們每次點擊Cause GC按鈕都會強制應用程序進行垃圾回收,并將清理后的數據顯示在Heap工具中。如下圖所示。
從上圖可以看出,Heap工具共有三個區域,分別是總覽視圖(上部)、詳情視圖(中部)和內存分配柱狀圖(下部)。
總覽視圖
其中總覽視圖可以查看整體的內存情況,表中的顯示信息如下所示:
- Heap Size 堆棧分配給該應用程序的內存大小
- Allocated 已使用的內存大小
- Free 空閑的內存大小
- %Used 當前Heap的使用率(Allocated/Heap Size)
- #Objects 對象的數量
詳情視圖
詳細視圖展示了所有的數據類型的內存情況,表中列的信息如下所示:
- Total Size 總共占用的內存大小
- Smallest 將該數據類型的對象從小到大排列,排在第一個的對象所占用的內存
- Largest 將該數據類型的對象從小到大排列,排在最后一個的對象所占用的內存
- Median 將該數據類型的對象從小到大排列,排在中間的對象所占用的內存
- Average 該數據類型的對象所占用內存的平均值
除了列的信息,還有行信息:
- data object 對象
- class object 類
- 1-byte array (byte[],boolean[]) 1字節的數組對象
- 2-byte array (short[],char[]) 2字節的數組對象
- 4-byte array (object[],int[],float[]) 4字節的數組對象
- 6-byte array (long[],double[]) 8字節的數組對象
- non-Java object 非Java對象
行信息中兩個比較重要的參數:
free—它與總覽視圖中的free的含義不同,它代表內存碎片。當新創建一個對象時,如果碎片內存能容下該對象,則復用碎片內存,否則就會從free空間(總覽視圖中的free)重新劃分內存給這個新對象。free是判斷內存碎片化程度的一個重要的指標。
1-byte array—圖片是以byte[]的形式存儲在內存中的,如果1-byte array一行的數據過大,則需要檢查圖片的內存管理了。
檢測內存泄漏
我們先寫一個內存泄漏的例子:
MainActivity:
SecondActivity:
public class SecondActivity extends AppCompatActivity {private static Object inner;private Button button;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button = (Button) findViewById(R.id.bt_next);button.setText("MainActivity");button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {createInnerClass();finish();}});}class InnerClass {}private void createInnerClass() {inner = new InnerClass();} }內存泄漏的原因很簡單,SecondActivity的內部類InnerClass Hold住了外部類實例的引用,而InnerClass的實例是靜態的,就會間接的長期維持著外部類實例的引用,阻止被系統回收,導致SecondActivity實例不能被釋放。
Heap Dump檢測內存泄漏:通常做法是使用Update Heap進行內存監聽,然后操作可能發生泄漏的APP功能、界面,并點擊Cause GC進行手動GC,經過多次操作后查看data object的Total Size大小是否有很大的變化,如果有則可能發生了內存泄漏,導致內存使用不斷增大。
步驟:
(1)在左邊Devices列表中選擇要查看的應用程序進程,點擊Update Heap按鈕,在右邊選擇Heap選項,并點擊Cause GC按鈕,就會開始顯示數據,如下圖所示。
(2)在MainActivity和SecondActivity間跳轉多次。這樣會生成多個SecondActivity實例且不能釋放。重新點擊Update Heap和Cause GC按鈕,顯示新的數據。
可以看到data object由610.500KB增長到1.158MB
(3)這時我點擊Cause GC按鈕,數據顯示為:
經過Cause GC的操作,Total Size的值從1.158MB變為了667.109KB,這是一個比較大的變化,說明在Cause GC操作之前有518.683KB(1.158MB-667.109KB)的內存沒有被回收,可能發生了內存泄漏。你多GC幾次,甚至會釋放更多的內存。
Allocation Tracker
使用Heap Dump可以讓你對APP的內存整體使用情況進行掌控,但缺點是無法了解每塊內存具體分配給哪個對象了,這時就需要使用Allocation Tracker工具來進行內存跟蹤。它允許你在執行某些操作的同時監視在何處分配對象,了解這些分配使你能夠調整與這些操作相關的方法調用,以優化應用程序性能和內存使用。
Allocation Tracker能夠做到如下的事情:
- 顯示代碼分配對象類型、大小、分配線程和堆棧跟蹤的時間和位置。
- 通過重復的分配/釋放模式幫助識別內存變化。
- 當與 HPROF Viewer結合使用時,可以幫助你跟蹤內存泄漏。例如,如果你在堆上看到一個bitmap對象,你可以使用Allocation Tracker來找到其分配的位置。
使用Allocation Tracker
AS和DDMS中都有Allocation Tracker,這里會介紹AS中的Allocation Tracke如何使用。
使用的步驟為:
- 運行需要監控的應用程序。
- 點擊AS面板下面的Android Monitors選項,查看
- 點擊Start Allocation Tracking按鈕
- 操作應用程序。
點擊Stop Allocation Tracking按鈕 ,結束快照。這時Memory Monitor會顯示出捕獲快照的期間,如下圖所示。
過幾秒后就會自動打開一個窗口,顯示當前生成的alloc文件的內存數據。
alloc文件分析
該alloc文件顯示以下信息:
- Method—負責分配的Java方法
- Count—分配的實例總數
- Total Size—分配內存的總字節數
目前的菜單選項是Group by Method我們也可以選擇 Group By Allocator,如下圖所示
同樣是上面的demo,我們在MainActivity和SecondActivity間跳轉了5次。
可以看到SecondActivity生成了5個匿名內部類OnClickListener實例(SecondActivity$1 表示它的第一個匿名內部類)和5個內部類InnerClass的實例,每個實例16個字節,且都沒有被釋放內存。
我們可以選擇列表中的一項,單擊鼠標右鍵,在彈出的菜單中選擇jump to the source就可以跳轉到對應的源文件中。
除此之外,還可以點擊Show/Hide Chart按鈕來顯示數據的圖形化,如下圖所示。
MAT
如果想要深入的進行分析并確定內存泄漏,就要分析疑似發生內存泄漏時所生成堆存儲文件。堆存儲文件可以使用DDMS或者Memory Monitor來生成,輸出的文件格式為hprof,而MAT就是來分析堆存儲文件的。
MAT,全稱為Memory Analysis Tool,是對內存進行詳細分析的工具,它是Eclipse的插件,如果用Android Studio進行開發則需要單獨下載它,下載地址為:http://eclipse.org/mat/。
生成hprof文件
我們這里分析一下用AS的Memory Monitor來生成hprof文件。
我們還是先寫一個內存泄漏的例子:
public class MainActivity extends AppCompatActivity {private Button button;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button =(Button)findViewById(R.id.bt_next);button.setText("SecondActivity");button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {startActivity(new Intent(MainActivity.this,SecondActivity.class));}});} } public class SecondActivity extends AppCompatActivity {private Button button;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button = (Button) findViewById(R.id.bt_next);button.setText("MainActivity");button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {LeakThread leakThread = new LeakThread();leakThread.start();finish();}});}class LeakThread extends Thread {@Overridepublic void run() {try {Thread.sleep(60 * 60 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}} }內存泄漏的原因是非靜態內部類LeakThread Hold住了外部類的引用,而LeakThread中做了耗時操作,導致外部類SecondActivity無法被釋放。
生成hprof文件主要分為一下幾個步驟:
- 運行需要監控的應用程序。
- 點擊AS面板下面的Android Monitors選項,查看
- 操作應用程序(本文的例子就是不斷切換Activity)。
- 點擊Dump Java Heap按鈕 ,生成hprof文件。
Memory Monitor生成的hprof文件不是標準的,AS提供了便捷的轉換方式:Memory Monitor生成的hprof文件都會顯示在AS左側的Captures標簽中,在Captures標簽中選擇要轉換的hprof文件,并點擊鼠標右鍵,在彈出的菜單中選擇Export to standard.hprof選項,即可導出標準的hprof文件,如下圖所示。
MAT分析hpof文件
用MAT打開標準的hprof文件,選擇Leak Suspects Report選項。這時MAT就會生成報告,這個報告分為兩個標簽頁,一個是Overview,一個是Leak Suspects,如下圖所示。
Leak Suspects中會給出了MAT認為可能出現內存泄漏問題的地方,本例共給出了4個內存泄漏猜想,通過點擊每個內存泄漏猜想的Details可以看到更深入的分析清理情況。如果內存泄漏不是特別的明顯,通過Leak Suspects是很難發現內存泄漏的位置。
打開Overview標簽頁,首先看到的是一個餅狀圖,它主要用來顯示內存的消耗,餅狀圖的彩色區域代表被分配的內存,灰色區域的則是空閑內存,點擊每個彩色區域可以看到這塊區域的詳細信息,如下圖所示。
再往下看,Actions一欄的下面列出了MAT提供的四種Action,其中分析內存泄漏最常用的就是Histogram和Dominator Tree。Histogram可以統計內存中對象的名稱、種類、實例數和大小,而Dominator Tree則是建立這些內存對象之間的關系。
我們點擊Actions中給出的鏈接或者在MAT工具欄中就可以打開Histogram和Dorminator Tree。
Histogram
圖中可以看出Dorminator Tree有四列數據。
- Class Name:類名
- Objects:對象實例的個數
- Shallow Heap:對象自身占用的內存大小,不包括它引用的對象。如果是數組類型的對象,它的大小是數組元素的類型和數組長度決定。如果是非數組類型的對象,它的大小由其成員變量的數量和類型決定。
- Retained Heap:一個對象的Retained Set所包含對象所占內存的總大小。換句話說,Retained Heap就是當前對象被GC后,從Heap上總共能釋放掉的內存。
在列表頂部的Regex區域,可以輸入過濾條件(支持正則表達式),通常Activity的內存泄漏,可以直接通過輸入Activity名獲取與之相關的的實例。
可以看到,SecondActivity實例創建了11次,基本可以判斷內存泄漏了。具體是如何泄漏的呢?可以通過查看GC對象的引用鏈來分析。在SecondActivity上右鍵,選擇Merge Shortest Paths to GC Root,并通過彈出的列表選擇相關類型的引用(強、軟、弱、虛),分析不同引用類型下的GC情況,這里我們選擇exclude all phantom/weak/soft etc. references,因為這個選項排除了虛引用、弱引用和軟引用,這些引用一般是可以被回收的。這時MAT就會給出SecondActivity的GC引用鏈。
到這里整個內存泄漏一目了然了,引用SecondActivity的是內部類LeakThread,this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是SecondActivity,導致SecondActivity無法被GC。
同時,在Histogram中還可以查看一個對象包含了哪些對象的引用。例如查看SecondActivity包含的引用,在SecondActivity上右鍵,選擇List objects—with incoming references(顯示選中對象被哪些外部對象引用,而with outcoming references表示選中對象持有哪些對象的引用)
Dominator Tree
Dorminator Tree意味支配樹,從名稱就可以看出Dorminator Tree更善于去分析對象的引用關系。而Histogram更側重于量的分析。
Shallow Heap和Retained Heap的含義和上面Histogram中的一樣。
同樣過濾SecondActivity:
發現有些圖標帶有小圓點,表示它們可以被GC系統訪問到,是內存泄漏的重點懷疑對象。那么SecondActivity沒有原點,是不是代表不能被GC訪問,可以回收呢?當然不是,如果可以回收,又怎么會存在這么多的實例呢。那怎么找到它的GC Root呢?在SecondActivity上右鍵,選擇Path To GC Roots,同樣選擇exclude all phantom/weak/soft etc. references
得出的結果和上面是一樣的,引用SecondActivity的是LeakThread,這導致了SecondActivity無法被GC。
OQL
OQL全稱為Object Query Language,類似于SQL語句的查詢語言,能夠用來查詢當前內存中滿足指定條件的所有的對象。它的查詢語句的基本格式為:
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression>]當我們點擊OQL按鈕,輸入條件select * from instanceof android.app.Activity并按下F5時(或者按下工具欄的紅色嘆號),會將當前內存中所有Activity都顯示出來,如下圖所示。
更過用法詳見官方文檔。
LeakCanary
LeakCanary 是一個開源的在debug版本中檢測內存泄漏的java庫。
使用LeakCanary
在APP的build.gradle文件添加:
dependencies {debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2' }接下來在Application加入如下代碼
public class BaseApplication extends Application {@Override public void onCreate() {super.onCreate();//如果當前的進程是用來給LeakCanary進行堆分析的則returnif (LeakCanary.isInAnalyzerProcess(this)) {return;}LeakCanary.install(this);} }上面代碼只能夠檢測Activity的內存泄漏,當然還存在其他類的內存泄漏,這時我們就需要使用RefWatcher來進行監控。改寫Application,如下所示
public class BaseApplication extends Application {private RefWatcher refWatcher;@Override public void onCreate() {super.onCreate();refWatcher= setupLeakCanary();}private RefWatcher setupLeakCanary() {//如果當前的進程是用來給LeakCanary進行堆分析的則returnif (LeakCanary.isInAnalyzerProcess(this)) {return RefWatcher.DISABLED;}return LeakCanary.install(this);}public static RefWatcher getRefWatcher(Context context) {BaseApplication baseApplication = (BaseApplication) context.getApplicationContext();return baseApplication.refWatcher;} }這里我們仍然使用上一節的demo,只是在SecondActivity中實現onDestroy方法,其中得到RefWatcher,并調用它的watch方法,watch方法的參數就是要監控的對象。
public class SecondActivity extends AppCompatActivity {private Button button;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button = (Button) findViewById(R.id.bt_next);button.setText("MainActivity");button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {LeakThread leakThread = new LeakThread();leakThread.start();finish();}});}class LeakThread extends Thread {@Overridepublic void run() {try {Thread.sleep(60 * 60 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}}@Overrideprotected void onDestroy() {super.onDestroy();RefWatcher refWatcher = BaseApplication.getRefWatcher(this);refWatcher.watch(this);} }其實這個例子中onDestroy方法是多余的,因為LeakCanary在調用install方法時會啟動一個ActivityRefWatcher類,它用于自動監控Activity執行onDestroy方法之后是否發生內存泄露。這里只是為了方便舉例,如果想要監控Fragment,在Fragment中添加如上的onDestroy方法是有用的。
操作
運行程序,這時會在界面生成一個名為Leaks的應用圖標。接下來不斷的切換Activity,這時會閃出一個提示框,提示內容為:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,內存泄漏信息就會通過Notification展示出來
Notification中提示了MainActivity發生了內存泄漏, 泄漏的內存為4.3KB。點擊Notification就可以進入內存泄漏詳細頁,除此之外也可以通過Leaks應用的列表界面進入,列表界面如下圖所示
點擊加號就可以查看具體類所在的包名稱。整個詳情就是一個引用鏈:SecondActiviy的內部類LeakThread引用了LeakThread的this0,this0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最后一行所給出的SecondActiviy的實例,這將會導致SecondActiviy無法被GC,從而產生內存泄漏。
除此之外,我們還可以將 heap dump(hprof文件)和info信息分享出去,如下圖所示。
需要注意的是分享出去的hprof文件并不是標準的hprof文件,還需要將它轉換為標準的hprof文件,這樣才會被MAT識別從而進行分析。
總結
以上是生活随笔為你收集整理的Android 内存检测工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows和Linux下排查C++软
- 下一篇: Android内存检测工具