Android 內存優化 (1) - MAT 使用入門

本人博客地址:https://androidperformance.com
本文博客地址:https://androidperformance.com/2015/04/11/AndroidMemory-Usage-Of-MAT/

系列文章

  1. Android 內存優化 (1) - MAT 使用入門
  2. Android 內存優化之二 - MAT使用進階
  3. Android 內存優化之三 - 打開 MAT 中的 Bitmap 原圖

MAT 簡介

MAT(Memory Analyzer Tool),一個基于Eclipse的內存分析工具,是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。使用內存分析工具從眾多的對象中進行分析,快速的計算出在內存中對象的占用大小,看看是誰阻止了垃圾收集器的回收工作,并可以通過報表直觀的查看到可能造成這種結果的對象。

MAT

當然MAT也有獨立的不依賴Eclipse的版本,只不過這個版本在調試Android內存的時候,需要將DDMS生成的文件進行轉換,才可以在獨立版本的MAT上打開。不過Android SDK中已經提供了這個Tools,所以使用起來也是很方便的。

MAT工具的下載安裝

這里是MAT的下載地址:https://eclipse.org/mat/downloads.php,下載時會提供三種選擇的方式:

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掉。

常見的內存使用不當的情況

查詢數據庫沒有關閉游標

描述:
程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢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
       }
   }
} 

構造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的使用和優化,可以參考這兩篇文章:

  1. Using lists in Android (ListView) - Tutorial
  2. Making ListView Scrolling Smooth

Bitmap對象不在使用時調用recycle()釋放內存

描述:有時我們會手工的操作Bitmap對象,如果一個Bitmap對象比較占內存,當它不在被使用的時候,可以調用Bitmap.recycle()方法回收此對象的像素所占用的內存。
另外在最新版本的Android開發時,使用下面的方法也可以釋放此Bitmap所占用的內存

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

釋放對象的引用

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

示例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的界面。

  1. 打開經過轉換的hprof文件:
open hprof

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

Leak Suspects

  1. 選擇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

Path To GC Roots

參考文檔

  1. Shallow and retained sizes
  2. MAT的wiki:http://wiki.eclipse.org/index.php/MemoryAnalyzer
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容