Android內存使用分析及MAT工具使用【推薦學習】

Android內存使用分析及MAT工具使用

          原文:http://blog.csdn.net/guolin_blog/article/details/42238633/      作者:guolin    

          推薦其他文章:Android內存泄漏分析及調試              Android的oom詳解

       由於Android是爲移動設備開發的操作系統,我們在開發應用程序的時候應當始終把內存問題充分考慮在內。雖然android系統擁有垃圾自動回收機制,但這並不意味着我們就可以完全忽略何時去分配或釋放內存。即使我們全部按照上一篇文章中給出的編程建議來去編寫程序,還是會很有可能出現內存泄露或其它類型的內存問題。所以,唯一能夠解決問題的辦法,就是嘗試去分析應用程序的內存使用情況,那麼本篇文章就會教大家如何進行分析。如果你還沒有看過前面一篇文章,建議先去閱讀 Android最佳性能實踐(一)——合理管理內存 。

雖說現在的手機內存都已經非常大了,但是我們大家都知道,系統是不可能將所有的內存都分配給我們的應用程序的。沒錯,每個程序都會有可使用的內存上限,這被稱爲堆大小(Heap Size)。不同的手機,堆大小也不盡相同,隨着現在硬件設備不斷提高,堆大小也已經由Nexus One時的32MB,變成了Nexus 5時的192MB。如果大家想要知道自己手機的堆大小是多少,可以調用如下代碼:

[java] view plain copy
  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);  
  2. int heapSize = manager.getMemoryClass();  

結果是以MB爲單位進行返回的,我們在開發應用程序時所使用的內存不能超出這個限制,否則就會出現OutOfMemoryError。因此,比如說我們的程序中需要緩存一些數據,就可以根據堆大小來決定緩存數據的容量。


下面我們來討論一下Android的GC操作,GC全稱是Garbage Collection,也就是所謂的垃圾回收。Android系統會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的對象進行回收。那麼哪些對象會被認爲是不再使用,並且可以被回收的呢?我們來看下面一張圖:




上圖當中,每個藍色的圓圈就代表一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關係。這些對象有些是處於活動狀態的,而有些就已經不再被使用了。那麼GC操作會從一個叫作Roots的對象開始檢查,所有它可以訪問到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經不再被使用了,如下圖所示:




可以看到,目前所有黃色的對象仍然會被系統繼續保留,而藍色的對象就會在GC操作當中被系統回收掉了,這大概就是Android系統一次簡單的GC流程。


那麼什麼時候會觸發GC操作呢?這個通常都是由系統去決定的,我們一般情況下都不需要主動通知系統應該去GC了(雖然我們確實可以這麼做,下面會講到),但是我們仍然可以去監聽系統的GC過程,以此來分析我們應用程序當前的內存狀態。那麼怎樣才能去監聽系統的GC過程呢?其實非常簡單,系統每進行一次GC操作時,都會在LogCat中打印一條日誌,我們只要去分析這條日誌就可以了,日誌的基本格式如下所示:

[plain] view plain copy
  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>  
注意這裏我仍然是以dalvik虛擬機來進行說明,art情況下打印的內容也是基本類似的。。


首先第一部分GC_Reason,這個是觸發這次GC操作的原因,一般情況下一共有以下幾種觸發GC操作的原因:

  • GC_CONCURRENT:   當我們應用程序的堆內存快要滿的時候,系統會自動觸發GC操作來釋放內存。
  • GC_FOR_MALLOC:   當我們的應用程序需要分配更多內存,可是現有內存已經不足的時候,系統會進行GC操作來釋放內存。
  • GC_HPROF_DUMP_HEAP:   當生成HPROF文件的時候,系統會進行GC操作,關於HPROF文件我們下面會講到。
  • GC_EXPLICIT:   這種情況就是我們剛纔提到過的,主動通知系統去進行GC操作,比如調用System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。

接下來第二部分Amount_freed,表示系統通過這次GC操作釋放了多少內存。


然後Heap_stats中會顯示當前內存的空閒比例以及使用情況(活動對象所佔內存 / 當前程序總內存)。


最後Pause_time表示這次GC操作導致應用程序暫停的時間。關於這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3之前GC操作是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。雖說這個阻塞的過程並不會很長,也就是幾百毫秒,但是用戶在使用我們的程序時還是有可能會感覺到略微的卡頓。而自2.3之後,GC操作改成了併發的方式進行,就是說GC的過程中不會影響到應用程序的正常運行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,用戶已經是完全無法察覺到了。


下面是一次GC操作在LogCat中打印的日誌:




可以看出,和我們上面所介紹的格式是完全一致的,最後的暫停時間31ms+7ms,一次就是GC開始時的暫停時間,一次是結束時的暫停時間。另外可以根據進程id來區分這是哪個程序中進行的GC操作,那麼從上圖就可以看出這條GC日誌是屬於24699這個程序的。


那麼這是使用dalvik運行環境時所打印的GC日誌,而自Android 4.4版本之後加入了art運行環境,在art中打印GC日誌基本和dalvik是相同的,如下圖所示:




相信沒有什麼難理解的地方吧,art中只是內容顯示的格式有了稍許變化,打印的主體內容仍然是不變的。


好的,通過日誌的方式我們可以簡單瞭解到系統的GC工作情況,但是如果我們想要更加清楚地實時知曉當前應用程序的內存使用情況,只通過日誌就有些力不從心了,我們需要通過DDMS中提供的工具來實現。


打開DDMS界面,在左側面板中選擇你要觀察的應用程序進程,然後點擊Update Heap按鈕,接着在右側面板中點擊Heap標籤,之後不停地點擊Cause GC按鈕來實時地觀察應用程序內存的使用情況即可,如下圖所示:




接着繼續操作我們的應用程序,然後繼續點擊Cause GC按鈕,如果你發現反覆操作某一功能會導致應用程序內存持續增高而不會下降的話,那麼就說明這裏很有可能發生內存泄漏了。


好了,討論完了GC,接下來我們討論一下Android中內存泄漏的問題。大家需要知道的是,Android中的垃圾回收機制並不能防止內存泄漏的出現,導致內存泄漏最主要的原因就是某些長存對象持有了一些其它應該被回收的對象的引用,導致垃圾回收器無法去回收掉這些對象,那也就出現內存泄漏了。比如說像Activity這樣的系統組件,它又會包含很多的控件甚至是圖片,如果它無法被垃圾回收器回收掉的話,那就算是比較嚴重的內存泄漏情況了。


下面我們來模擬一種Activity內存泄漏的場景,內部類相信大家都有用過,如果我們在一個類中又定義了一個非靜態的內部類,那麼這個內部類就會持有外部類的引用,如下所示:

[java] view plain copy
  1. public class MainActivity extends ActionBarActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LeakClass leakClass = new LeakClass();  
  8.     }  
  9.   
  10.     class LeakClass {  
  11.   
  12.     }  
  13.     ......  
  14. }  
目前來看,代碼還是沒有問題的,因爲雖然LeakClass這個內部類持有MainActivity的引用,但是隻要它的存活時間不會長於MainActivity,就不會阻止MainActivity被垃圾回收器回收。那麼現在我們來將代碼進行如下修改:
[java] view plain copy
  1. public class MainActivity extends ActionBarActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LeakClass leakClass = new LeakClass();  
  8.         leakClass.start();  
  9.     }  
  10.   
  11.     class LeakClass extends Thread {  
  12.   
  13.         @Override  
  14.         public void run() {  
  15.             while (true) {  
  16.                 try {  
  17.                     Thread.sleep(60 * 60 * 1000);  
  18.                 } catch (InterruptedException e) {  
  19.                     e.printStackTrace();  
  20.                 }  
  21.             }  
  22.         }  
  23.     }  
  24.     ......  
  25. }  
這下就有點不太一樣了,我們讓LeakClass繼承自Thread,並且重寫了run()方法,然後在MainActivity的onCreate()方法中去啓動LeakClass這個線程。而LeakClass的run()方法中運行了一個死循環,也就是說這個線程永遠都不會執行結束,那麼LeakClass這個對象就一直不能得到釋放,並且它持有的MainActivity也將無法得到釋放,那麼內存泄露就出現了。


現在我們可以將程序運行起來,然後不斷地旋轉手機讓程序在橫屏和豎屏之間切換,因爲每切換一次Activity都會經歷一個重新創建的過程,而前面創建的Activity又無法得到回收,那麼長時間操作下我們的應用程序所佔用的內存就會越來越高,最終出現OutOfMemoryError。


下面我貼出一張不斷切換橫豎屏時GC日誌打印的結果圖,如下所示:




可以看到,應用程序所佔用的內存是在不斷上升的。最可怕的是,這些內存一旦升上去了就永遠不會再降下來,直到程序崩潰爲止,因爲這部分泄露的內存一直都無法被垃圾回收器回收掉。


那麼通過上面學習的GC日誌以及DDMS工具這兩種方式,現在我們已經可以比較輕鬆地發現應用程序中是否存在內存泄露的現象了。但是如果真的出現了內存泄露,我們應該怎麼定位到具體是哪裏出的問題呢?這就需要藉助一個內存分析工具了,叫做Eclipse Memory Analyzer(MAT)。我們需要先將這個工具下載下來,下載地址是:http://eclipse.org/mat/downloads.php。這個工具分爲Eclipse插件版和獨立版兩種,如果你是使用Eclipse開發的,那麼可以使用插件版MAT,非常方便。如果你是使用Android Studio開發的,那麼就只能使用獨立版的MAT了。


下載好了之後下面我們開始學習如何去分析內存泄露的原因,首先還是進入到DDMS界面,然後在左側面板選中我們要觀察的應用程序進程,接着點擊Dump HPROF file按鈕,如下圖所示:




點擊這個按鈕之後需要等待一段時間,然後會生成一個HPROF文件,這個文件記錄着我們應用程序內部的所有數據。但是目前MAT還是無法打開這個文件的,我們還需要將這個HPROF文件從Dalvik格式轉換成J2SE格式,使用hprof-conv命令就可以完成轉換工作,如下所示:

[plain] view plain copy
  1. hprof-conv dump.hprof converted-dump.hprof  

hprof-conv命令文件存放於<Android Sdk>/platform-tools目錄下面。另外如果你是使用的插件版的MAT,也可以直接在Eclipse中打開生成的HPROF文件,不用經過格式轉換這一步。


好的,接下來我們就可以來嘗試使用MAT工具去分析內存泄漏的原因了,這裏需要提醒大家的是,MAT並不會準確地告訴我們哪裏發生了內存泄漏,而是會提供一大堆的數據和線索,我們需要自己去分析這些數據來去判斷到底是不是真的發生了內存泄漏。那麼現在運行MAT工具,然後選擇打開轉換過後的converted-dump.hprof文件,如下圖所示:




MAT中提供了非常多的功能,這裏我們只要學習幾個最常用的就可以了。上圖最中央的那個餅狀圖展示了最大的幾個對象所佔內存的比例,這張圖中提供的內容並不多,我們可以忽略它。在這個餅狀圖的下方就有幾個非常有用的工具了,我們來學習一下。


Histogram可以列出內存中每個對象的名字、數量以及大小。

Dominator Tree會將所有內存中的對象按大小進行排序,並且我們可以分析對象之間的引用結構。


一般最常用的就是以上兩個功能了,那麼我們先從Dominator Tree開始學起。


現在點擊Dominator Tree,結果如下圖所示:




這張圖包含的信息非常多,我來帶着大家一起解析一下。首先Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存,因此從上圖中看,前兩行的Retained Heap是最大的,我們分析內存泄漏時,內存最大的對象也是最應該去懷疑的。


另外大家應該可以注意到,在每一行的最左邊都有一個文件型的圖標,這些圖標有的左下角帶有一個紅色的點,有的則沒有。帶有紅點的對象就表示是可以被GC Roots訪問到的,根據上面的講解,可以被GC Root訪問到的對象都是無法被回收的。那麼這就說明所有帶紅色的對象都是泄漏的對象嗎?當然不是,因爲有些對象系統需要一直使用,本來就不應該被回收。我們可以注意到,上圖當中所有帶紅點的對象最右邊都有寫一個System Class,說明這是一個由系統管理的對象,並不是由我們自己創建並導致內存泄漏的對象。


那麼上圖中就無法看出內存泄漏的原因了嗎?確實,內存泄漏本來就不是這麼容易找出的,我們還需要進一步進行分析。上圖當中,除了帶有System Class的行之外,最大的就是第二行的Bitmap對象了,雖然Bitmap對象現在不能被GC Roots訪問到,但不代表着Bitmap所持有的其它引用也不會被GC Roots訪問到。現在我們可以對着第二行點擊右鍵 -> Path to GC Roots -> exclude weak references,爲什麼選擇exclude weak references呢?因爲弱引用是不會阻止對象被垃圾回收器回收的,所以我們這裏直接把它排除掉,結果如下圖所示:




可以看到,Bitmap對象經過層層引用之後,到了MainActivity$LeakClass這個對象,然後在圖標的左下角有個紅色的圖標,就說明在這裏可以被GC Roots訪問到了,並且這是由我們自己創建的Thread,並不是System Class了,那麼由於MainActivity$LeakClass能被GC Roots訪問到導致不能被回收,導致它所持有的其它引用也無法被回收了,包括MainActivity,也包括MainActivity中所包含的圖片。


通過這種方式,我們就成功地將內存泄漏的原因找出來了。這是Dominator Tree中比較常用的一種分析方式,即搜索大內存對象通向GC Roots的路徑,因爲內存佔用越高的對象越值得懷疑。


接下來我們再來學習一下Histogram的用法,回到Overview界面,點擊Histogram,結果如下圖所示:




這裏是把當前應用程序中所有的對象的名字、數量和大小全部都列出來了,需要注意的是,這裏的對象都是隻有Shallow Heap而沒有Retained Heap的,那麼Shallow Heap又是什麼意思呢?就是當前對象自己所佔內存的大小,不包含引用關係的,比如說上圖當中,byte[]對象的Shallow Heap最高,說明我們應用程序中用了很多byte[]類型的數據,比如說圖片。可以通過右鍵 -> List objects -> with incoming references來查看具體是誰在使用這些byte[]。


那麼通過Histogram又怎麼去分析內存泄漏的原因呢?當然其實也可以用和Dominator Tree中比較相似的方式,即分析大內存的對象,比如上圖中byte[]對象內存佔用很高,我們通過分析byte[],最終也是能找到內存泄漏所在的,但是這裏我準備使用另外一種更適合Histogram的方式。大家可以看到,Histogram中是可以顯示對象的數量的,那麼比如說我們現在懷疑MainActivity中有可能存在內存泄漏,就可以在第一行的正則表達式框中搜索“MainActivity”,如下所示:




可以看到,這裏將包含“MainActivity”字樣的所有對象全部列出了出來,其中第一行就是MainActivity的實例。但是大家有沒有注意到,當前內存中是有11個MainActivity的實例的,這太不正常了,通過情況下一個Activity應該只有一個實例纔對。其實這些對象就是由於我們剛纔不斷地橫豎屏切換所產生的,因爲橫豎屏切換一次,Activity就會經歷一個重新創建的過程,但是由於LeakClass的存在,之前的Activity又無法被系統回收,那麼就出現這種一個Activity存在多個實例的情況了。


接下來對着MainActivity右鍵 -> List objects -> with incoming references查看具體MainActivity實例,如下圖所示:




如果想要查看內存泄漏的具體原因,可以對着任意一個MainActivity的實例右鍵 -> Path to GC Roots -> exclude weak references,結果如下圖所示:




可以看到,我們再次找到了內存泄漏的原因,是因爲MainActivity$LeakClass對象所導致的。


好了,這大概就是MAT工具最常用的一些用法了,當然這裏還要提醒大家一句,工具是死的,人是活的,MAT也沒有辦法保證一定可以將內存泄漏的原因找出來,還是需要我們對程序的代碼有足夠多的瞭解,知道有哪些對象是存活的,以及它們存活的原因,然後再結合MAT給出的數據來進行具體的分析,這樣纔有可能把一些隱藏得很深的問題原因給找出來。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章