1. 程式人生 > >記憶體分析工具MAT的使用

記憶體分析工具MAT的使用

MAT簡介

MAT(Memory Analyzer Tool),一個基於Eclipse的記憶體分析工具,是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查詢記憶體洩漏和減少記憶體消耗。使用記憶體分析工具從眾多的物件中進行分析,快速的計算出在記憶體中物件的佔用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的檢視到可能造成這種結果的物件。

MAT

當然MAT也有獨立的不依賴Eclipse的版本,只不過這個版本在除錯Android記憶體的時候,需要將DDMS生成的檔案進行轉換,才可以在獨立版本的MAT上開啟。不過Android SDK中已經提供了這個Tools,所以使用起來也是很方便的。

MAT工具的下載安裝

Download MAT

Update Site 這種方式後面會有一個網址:比如http://download.eclipse.org/mat/1.4/update-site/ ,安裝過Eclipse外掛的同學應該知道,只要把這段網址複製到對應的Eclipse的Install New Software那裡,就可以進行線上下載了。

MAT with eclipse

Archived Update Site 這種方式安裝的位置和上一種差不多,只不過第一種是線上下載,這一種是使用離線包進行更新,這種方式劣勢是當這個外掛更新後,需要重新下載離線包,而第一種方式則可以線上下載更新。

Stand-alone Eclipse RCP Applications 這種方式就是把MAT當成一個獨立的工具使用,不再依附於Eclipse,適合不使用Eclipse而使用Android Studio的同學。這種方式有個麻煩的地方就是DDMS匯出的檔案,需要進行轉換才可以在MAT中開啟。

下載安裝好之後,就可以使用MAT進行實際的操作了。

Android(Java)中常見的容易引起記憶體洩露的不良程式碼

使用MAT工具之前,要對Android的記憶體分配方式有基本的瞭解,對容易引起記憶體洩露的程式碼也要保持敏感,在程式碼級別對記憶體洩露的排查,有助於記憶體的使用。

Android主要應用在嵌入式裝置當中,而嵌入式裝置由於一些眾所周知的條件限制,通常都不會有很高的配置,特別是記憶體是比較有限的。如果我們編寫的程式碼當中有太多的對記憶體使用不當的地方,難免會使得我們的裝置執行緩慢,甚至是宕機。為了能夠使得Android應用程式安全且快速的執行,Android的每個應用程式都會使用一個專有的Dalvik虛擬機器例項來執行,它是由Zygote服務程序孵化出來的,也就是說每個應用程式都是在屬於自己的程序中執行的。一方面,如果程式在執行過程中出現了記憶體洩漏的問題,僅僅會使得自己的程序被kill掉,而不會影響其他程序(如果是system_process等系統程序出問題的話,則會引起系統重啟)。另一方面Android為不同型別的程序分配了不同的記憶體使用上限,如果應用程序使用的記憶體超過了這個上限,則會被系統視為記憶體洩漏,從而被kill掉。

常見的記憶體使用不當的情況

1、查詢資料庫沒有關閉遊標

描述:程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。
示例程式碼:

Cursor cursor = getContentResolver().query(uri ...);
  if (cursor.moveToNext()) {
   ... ... 
}

修正示例程式碼:

Cursor cursor = null;
try {
    cursor = getContentResolver().query(uri ...);
  if (cursor != null && cursor.moveToNext()) {
  ... ... 
  }
  } finally {
      if (cursor != null) {
  try { 
      cursor.close();
  } catch (Exception e) {
      //ignore this
      }
  }
}

2、構造Adapter時,沒有使用快取的 convertView

描述:以構造ListView的BaseAdapter為例,在BaseAdapter中提供了方法:

public View getView(int position, View convertView, ViewGroup parent)

來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。

由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費資源也浪費時間,也會使得記憶體佔用越來越大。ListView回收list item的view物件的過程可以檢視:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。

示例程式碼:

public View getView(int position, View convertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}

示例修正程式碼:

public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
}

關於ListView的使用和優化,可以參考這兩篇文章:

3、Bitmap物件不在使用時呼叫recycle()釋放記憶體

描述:有時我們會手工的操作Bitmap物件,如果一個Bitmap物件比較佔記憶體,當它不在被使用的時候,可以呼叫Bitmap.recycle()方法回收此物件的畫素所佔用的記憶體。

另外在最新版本的Android開發時,使用下面的方法也可以釋放此Bitmap所佔用的記憶體

Bitmap bitmap ;
...
bitmap初始化以及使用
...
bitmap = null;

4、釋放物件的引用

描述:這種情況描述起來比較麻煩,舉兩個例子進行說明。

示例A:
假設有如下操作

public class DemoActivity extends Activity {
 ... ...
 private Handler mHandler = ...
 private Object obj;
 public void operation() {
  obj = initObj();
  ...
  [Mark]
  mHandler.post(new Runnable() {
         public void run() {
          useObj(obj);
         }
  });
 }
}

我們有一個成員變數 obj,在operation()中我們希望能夠將處理obj例項的操作post到某個執行緒的MessageQueue中。在以上的程式碼中,即便是mHandler所在的執行緒使用完了obj所引用的物件,但這個物件仍然不會被垃圾回收掉,因為DemoActivity.obj還保有這個物件的引用。所以如果在DemoActivity中不再使用這個物件了,可以在[Mark]的位置釋放物件的引用,而程式碼可以修改為:

public void operation() {
 obj = initObj();
 ...
 final Object o = obj;
 obj = null;
 mHandler.post(new Runnable() {
     public void run() {
         useObj(o);
     }
 }
}

示例B:
假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。

但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被垃圾回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得system_process程序掛掉。

總之當一個生命週期較短的物件A,被一個生命週期較長的物件B保有其引用的情況下,在A的生命週期結束時,要在B中清除掉對A的引用。

其他
Android應用程式中最典型的需要注意釋放資源的情況是在Activity的生命週期中,在onPause()、onStop()、onDestroy()方法中需要適當的釋放資源的情況。由於此情況很基礎,在此不詳細說明,具體可以檢視官方文件對Activity生命週期的介紹,以明確何時應該釋放哪些資源。

使用MAT進行記憶體除錯

要除錯記憶體,首先需要獲取HPROF檔案,HPROF檔案是MAT能識別的檔案,HPROF檔案儲存的是特定時間點,java程序的記憶體快照。有不同的格式來儲存這些資料,總的來說包含了快照被觸發時java物件和類在heap中的情況。由於快照只是一瞬間的事情,所以heap dump中無法包含一個物件在何時、何地(哪個方法中)被分配這樣的資訊。

使用Eclipse獲取HPROF檔案

這個檔案可以使用DDMS匯出,DDMS中在Devices上面有一排按鈕,選擇一個程序後(即在Devices下面列出的列表中選擇你要除錯的應用程式的包名),點選Dump HPROF file 按鈕:

Dump HEAP with DDMS

選擇儲存路徑儲存後就可以得到對應程序的HPROF檔案。eclipse外掛可以把上面的工作一鍵完成。只需要點選Dump HPROF file圖示,然後MAT外掛就會自動轉換格式,並且在eclipse中開啟分析結果。eclipse中還專門有個Memory Analysis檢視 ,得到對應的檔案後,如果安裝了Eclipse外掛,那麼切換到Memory Analyzer檢視。使用獨立安裝的,要使用Android SDK自帶的的工具(hprof-conv 位置在sdk/platform-tools/hprof-conv)進行轉換

hprof-conv xxx.xxx.xxx.hprof xxx.xxx.xxx.hprof

轉換過後的.hprof檔案即可使用MAT工具打開了。

使用Android Studio獲取HPROF檔案
使用Android Studio同樣可以匯出對應的HPROF檔案:

Android-Studio

最新版本的Android Studio得在檔案上右鍵轉換成標準的HPROF檔案,在可以在MAT中開啟。

MAT主介面介紹

這裡介紹的不是MAT這個工具的主介面,而是匯入一個檔案之後,顯示OverView的介面。

開啟經過轉換的hprof檔案:

open hprof

如果選擇了第一個,則會生成一個報告。這個無大礙。

Leak Suspects

選擇OverView介面:

System OverView

我們需要關注的是下面的Actions區域

Histogram:列出記憶體中的物件,物件的個數以及大小

Histogram

Dominator Tree:列出最大的物件以及其依賴存活的Object (大小是以Retained Heap為標準排序的)

Dominator Tree

Top Consumers : 通過圖形列出最大的object

Top Consumers

Duplicate Class:通過MAT自動分析洩漏的原因

一般Histogram和 Dominator Tree是最常用的。

MAT中一些概念介紹

要看懂MAT的列表資訊,Shallow heap、Retained Heap、GC Root這幾個概念一定要弄懂。

Shallow heap

Shallow size就是物件本身佔用記憶體的大小,不包含其引用的物件。

  • 常規物件(非陣列)的Shallow size有其成員變數的數量和型別決定。
  • 陣列的shallow size有陣列元素的型別(物件型別、基本型別)和陣列長度決定

因為不像c++的物件本身可以存放大量記憶體,java的物件成員都是些引用。真正的記憶體都在堆上,看起來是一堆原生的byte[], char[], int[],所以我們如果只看物件本身的記憶體,那麼數量都很小。所以我們看到Histogram圖是以Shallow size進行排序的,排在第一位第二位的是byte,char 。

Retained Heap

Retained Heap的概念,它表示如果一個物件被釋放掉,那會因為該物件的釋放而減少引用進而被釋放的所有的物件(包括被遞迴釋放的)所佔用的heap大小。於是,如果一個物件的某個成員new了一大塊int陣列,那這個int陣列也可以計算到這個物件中。相對於shallow heap,Retained heap可以更精確的反映一個物件實際佔用的大小(因為如果該物件釋放,retained heap都可以被釋放)。

這裡要說一下的是,Retained Heap並不總是那麼有效。例如我在A裡new了一塊記憶體,賦值給A的一個成員變數。此時我讓B也指向這塊記憶體。此時,因為A和B都引用到這塊記憶體,所以A釋放時,該記憶體不會被釋放。所以這塊記憶體不會被計算到A或者B的Retained Heap中。為了糾正這點,MAT中的Leading Object(例如A或者B)不一定只是一個物件,也可以是多個物件。此時,(A, B)這個組合的Retained Set就包含那塊大記憶體了。對應到MAT的UI中,在Histogram中,可以選擇Group By class, superclass or package來選擇這個組。

為了計算Retained Memory,MAT引入了Dominator Tree。加入物件A引用B和C,B和C又都引用到D(一個菱形)。此時要計算Retained Memory,A的包括A本身和B,C,D。B和C因為共同引用D,所以他倆的Retained Memory都只是他們本身。D當然也只是自己。我覺得是為了加快計算的速度,MAT改變了物件引用圖,而轉換成一個物件引用樹。在這裡例子中,樹根是A,而B,C,D是他的三個兒子。B,C,D不再有相互關係。把引用圖變成引用樹,計算Retained Heap就會非常方便,顯示也非常方便。對應到MAT UI上,在dominator tree這個view中,顯示了每個物件的shallow heap和retained heap。然後可以以該節點位樹根,一步步的細化看看retained heap到底是用在什麼地方了。要說一下的是,這種從圖到樹的轉換確實方便了記憶體分析,但有時候會讓人有些疑惑。本來物件B是物件A的一個成員,但因為B還被C引用,所以B在樹中並不在A下面,而很可能是平級。

為了糾正這點,MAT中點選右鍵,可以List objects中選擇with outgoing references和with incoming references。這是個真正的引用圖的概念,

outgoing references :表示該物件的出節點(被該物件引用的物件)。
incoming references :表示該物件的入節點(引用到該物件的物件)。
為了更好地理解Retained Heap,下面引用一個例子來說明:

把記憶體中的物件看成下圖中的節點,並且物件和物件之間互相引用。這裡有一個特殊的節點GC Roots,這就是reference chain(引用鏈)的起點:

Paste_Image.png

Paste_Image.png

從obj1入手,上圖中藍色節點代表僅僅只有通過obj1才能直接或間接訪問的物件。因為可以通過GC Roots訪問,所以左圖的obj3不是藍色節點;而在右圖卻是藍色,因為它已經被包含在retained集合內。
所以對於左圖,obj1的retained size是obj1、obj2、obj4的shallow size總和;
右圖的retained size是obj1、obj2、obj3、obj4的shallow size總和。
obj2的retained size可以通過相同的方式計算。

GC Root

GC發現通過任何reference chain(引用鏈)無法訪問某個物件的時候,該物件即被回收。名詞GC Roots正是分析這一過程的起點,例如JVM自己確保了物件的可到達性(那麼JVM就是GC Roots),所以GC Roots就是這樣在記憶體中保持物件可到達性的,一旦不可到達,即被回收。通常GC Roots是一個在current thread(當前執行緒)的call stack(呼叫棧)上的物件(例如方法引數和區域性變數),或者是執行緒自身或者是system class loader(系統類載入器)載入的類以及native code(原生代碼)保留的活動物件。所以GC Roots是分析物件為何還存活於記憶體中的利器。

MAT中的一些有用的檢視

Thread OvewView
Thread OvewView可以檢視這個應用的Thread資訊:

Thread OvewView

Group

在Histogram和Domiantor Tree介面,可以選擇將結果用另一種Group的方式顯示(預設是Group by Object),切換到Group by package,可以更好地檢視具體是哪個包裡的類佔用記憶體大,也很容易定位到自己的應用程式。

Group

Path to GC Root

在Histogram或者Domiantor Tree的某一個條目上,右鍵可以檢視其GC Root Path:

Path to GC Root

這裡也要說明一下Java的引用規則:
從最強到最弱,不同的引用(可到達性)級別反映了物件的生命週期。

  • Strong Ref 強引用
    通常我們編寫的程式碼都是Strong Ref,於此對應的是強可達性,只有去掉強可達,物件才被回收。
  • Soft Ref 軟引用
    對應軟可達性,只要有足夠的記憶體,就一直保持物件,直到發現記憶體吃緊且沒有Strong Ref時才回收物件。一般可用來實現快取,通過java.lang.ref.SoftReference類實現。
  • Weak Ref 弱引用
    比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收物件而不必等到記憶體吃緊的時候。通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現。
  • Phantom Ref 虛引用
    根本不會在記憶體中保持任何物件,你只能使用Phantom Ref本身。一般用於在進入finalize()方法後進行特殊的清理過程,通過 java.lang.ref.PhantomReference實現。

點選Path To GC Roots –> with all references

這裡寫圖片描述

擴充套件閱讀