Android 系統穩定性 - OOM(二)

2.3 如何分析內存溢出問題

無論怎麼小心,想完全避免 bad code 是不可能的,此時就需要一些工具來幫助我們檢查代碼中是否存在會造成內存泄漏的地方。

既然要排查的是內存問題,自然需要與內存相關的工具,DDMS和MAT就是兩個非常好的工具。下面詳細介紹。

2.3.1 內存監測工具 DDMS --> Heap

Android tools 中的 DDMS 就帶有一個很不錯的內存監測工具 Heap(這裏我使用 eclipse 的 ADT 插件,並以真機爲例,在模擬器中的情況類似)。用 Heap 監測應用進程使用內存情況的步驟如下:

  1. 啓動 eclipse 後,切換到 DDMS 透視圖,並確認 Devices 視圖、Heap 視圖都是打開的;

  2. 將手機通過 USB 鏈接至電腦,鏈接時需要確認手機是處於“USB 調試”模式,而不是作爲“Mass Storage”;

  3. 鏈接成功後,在 DDMS 的 Devices 視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;

  4. 點擊選中想要監測的進程,比如 system_process 進程;

  5. 點擊選中 Devices 視圖界面中最上方一排圖標中的“Update Heap”圖標;

  6. 點擊 Heap 視圖中的“Cause GC”按鈕;

  7. 此時在 Heap 視圖中就會看到當前選中的進程的內存使用量的詳細情況[如圖所示]。


 

說明:

  • 點擊“Cause GC”按鈕相當於向虛擬機請求了一次 gc 操作;

  • 當內存使用信息第一次顯示以後,無須再不斷的點擊“Cause GC”,Heap 視圖界面會定時刷新,在對應用的不斷的操作過程中就可以看到內存使用的變化;

  • 內存使用信息的各項參數根據名稱即可知道其意思,在此不再贅述。如何才能知道我們的程序是否有內存泄漏的可能性呢。這裏需要注意一個值:Heap 視圖中部有一個 Type 叫做 data object,即數據對象,也就是我們的程序中大量存在的類類型的對象。在 data object 一行中有一列是“Total Size”,其值就是當前進程中所有 Java 數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:

  • 不斷的操作當前應用,同時注意觀察 data object 的 Total Size 值;

  • 正常情況下 Total Size 值都會穩定在一個有限的範圍內,也就是說由於程序中的代碼良好,沒有造成對象不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對象 ,而在虛擬機不斷的進行GC 的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;

  • 反之如果代碼中存在沒有釋放對象引用的情況,則 data object 的 Total Size 值在每次 GC後不會有明顯的回落,隨着操作次數的增多 Total Size 的值會越來越大,直到到達一個上限後導致進程被 kill掉。

  • 此處已 system_process 進程爲例,在我的測試環境中 system_process 進程所佔用的內存的data object 的 Total Size 正常情況下會穩定在 2.2~2.8 之間,而當其值超過 3.55 後進程就會被kill。

總之,使用 DDMS 的 Heap 視圖工具可以很方便的確認我們的程序是否存在內存泄漏的可能性。

2.3.2 內存分析工具 MAT(Memory Analyzer Tool)

如果使用 DDMS 確實發現了我們的程序中存在內存泄漏,那又如何定位到具體出現問題的代碼片段,最終找到問題所在呢?如果從頭到尾的分析代碼邏輯,那肯定會把人逼瘋,特別是在維護別人寫的代碼的時候。這裏介紹一個極好的內存分析工具 -- Memory Analyzer Tool(MAT)。

MAT 是一個 Eclipse 插件,同時也有單獨的 RCP 客戶端。官方下載地址、MAT 介紹和詳細的使用教程請參見:www.eclipse.org/mat,在此不進行說明了。另外在 MAT 安裝後的幫助文檔裏也有完備的使用教程。在此僅舉例說明其使用方法。我自己使用的是 MAT 的eclipse 插件,使用插件要比RCP 稍微方便一些。

MAT通過解析Hprof文件來分析內存使用情況。HPROF其實是在J2SE5.0中包含的用來分析CPU使用和堆內存佔用的日誌文件,實質上是虛擬機在某一時刻的內存快照,dalvik中也包含了這樣的工具,但是其文件格式和JVM的格式不完全相同,可以用SDK中自帶的hprof-conv工具進行轉換,例如:

$./hprof-conv raw.hprof converted.hprof

可以使用hprof文件配合traceview來分析CPU使用情況(函數調用時間),此處僅僅討論用它來分析內存使用情況,關於hprof的其他信息可以查看:http://java.sun.com/developer/technicalArticles/Programming/HPROF.html

以及Android源碼中的/dalvik/docs/heap-profiling.html文件(這個比較重要,建議看看,例如kill -10在Android2.3中已經不支持了)。

使用 MAT 進行內存分析需要幾個步驟,包括:生成.hprof 文件、打開 MAT 並導入hprof文件、使用MAT 的視圖工具分析內存。以下詳細介紹。

1. 生成hprof 文件

生成hprof 文件的方法有很多,而且 Android 的不同版本中生成hprof 的方式也稍有差別,我使用的版本的是 2.1,各個版本中生成hprof 文件的方法請參考:

http://android.git.kernel.org/?p=platform/dalvik.git;a=blob_plain;f=docs/heap-profiling.html;hb=HEAD。

(1) 打開 eclipse 並切換到 DDMS 透視圖,同時確認 Devices、Heap 和 logcat 視圖已經打開了 ;

(2) 將手機設備鏈接到電腦,並確保使用“USB 調試”模式鏈接,而不是“Mass Storage“模式;

(3) 鏈接成功後在 Devices 視圖中就會看到設備的序列號,和設備中正在運行的部分進程;

(4) 點擊選中想要分析的應用的進程,在 Devices 視圖上方的一行圖標按鈕中,同時選中“Update Heap”和“Dump HPROF file”兩個按鈕;

(5) 這是 DDMS 工具將會自動生成當前選中進程的.hprof 文件,並將其進行轉換後存放在sdcard當中,如果你已經安裝了 MAT 插件,那麼此時 MAT 將會自動被啓用,並開始對.hprof文件進行分析;

注意: (4)步和第(5)步能夠正常使用前提是我們需要有 sdcard,並且當前進程有向 sdcard中寫入的權限(WRITE_EXTERNAL_STORAGE),否則.hprof 文件不會被生成,在 logcat 中會顯示諸如ERROR/dalvikvm(8574): hprof: can't open /sdcard/com.xxx.hprof-hptemp: Permission denied.的信息。

如果我們沒有 sdcard,或者當前進程沒有向 sdcard 寫入的權限(如 system_process) 那我們可以這樣做:

(6) 在當前程序中,例如 framework 中某些代碼中,可以使用 android.os.Debug 中的:

public static void dumpHprofData(String fileName) throws IOException

方法,手動的指定.hprof 文件的生成位置。例如:

xxxButton.setOnClickListener(new View.OnClickListener() {

        public void onClick(View view) {

                android.os.Debug.dumpHprofData("/data/temp/myapp.hprof");

                ... ...

        }

}

上述代碼意圖是希望在 xxxButton 被點擊的時候開始抓取內存使用信息,並保存在我們指定的位置:/data/temp/myapp.hprof,這樣就沒有權限的限制了,而且也無須用 sdcard。但要保證/data/temp 目錄是存在的。這個路徑可以自己定義,當然也可以寫成 sdcard 當中的某個路徑。

如果不確定進程什麼時候會OOM,例如我們在跑Monkey的過程中出現了OOM,此時最好的辦法就是讓程序在出現OOM之後,而沒有將OOM的錯誤信息拋給虛擬機之前就將進程的hprof抓取出來。方法也很簡單,只需要在代碼中你認爲會拋出OutOfMemoryError的地方try...catch,並在catch塊中使用android.os.Debug.dumpHprofData(String file)方法就可以請求虛擬機dump出hprof到你指定的文件中。例如我們之前爲了排查應用進程主線程中發生的OOM,就在ActivityThread.main()方法中添加了以下代碼:

try {

        Looper.loop();

} catch (OutOfMemoryError e) {

        String file = "path_to_file.hprof"

        ... ...

        try {

                android.os.Debug.dumpHprofData(file);

        } catch (IOException e1) {

                e1.printStackTrace();

        }

}

在設置hprof的文件路徑時,需要考慮權限問題,包括SD卡訪問權限、/data分區私有目錄訪問權限。

之所以在以上位置添加代碼,是因爲在應用進程主線程中如果發生異常和錯誤沒有捕獲,最終都會從Looper.loop()中拋出來。如果你需要排查在其他線程,或者framework中的OOM問題時,同樣可以在適當的位置使用android.os.Debug.dumpHprofData(String file)方法dump hprof文件。

有了hprof文件,並且用hprof-conv轉換格式之後,第二步就可以用MemoryAnalyzerTool(MAT)工具來分析內存使用情況了。

2. 使用 MAT 導入hprof 文件

(1) 如果是 eclipse 自動生成的hprof 文件,可以使用 MAT 插件直接打開(可能是比較新的 ADT才支持);

(2) 如 果 eclipse 自 動 生 成 的 .hprof 文 件 不 能 被 MAT 直 接 打 開 , 或 者 是 使 用android.os.Debug.dumpHprofData()方法手動生成的hprof 文件,則需要將hprof 文件進行轉換,轉換的方法:

例如我將hprof 文件拷貝到 PC 上的/ANDROID_SDK/tools 目錄下,並輸入命令 hprof-conv xxx.hprof yyy.hprof,其中 xxx.hprof 爲原始文件,yyy.hprof 爲轉換過後的文件。轉換過後的文件自動放在/ANDROID_SDK/tools 目錄下。OK,到此爲止,hprof 文件處理完畢,可以用來分析內存泄露情況了。

(3) 在 Eclipse 中點擊 Windows->Open Perspective->Other->Memory Analyzer,或者打 Memory Analyzer Tool 的 RCP。在 MAT 中點擊 File->Open File,瀏覽並導入剛剛轉換而得到的hprof文件。

3. 使用 MAT 的視圖工具分析內存

導入hprof 文件以後,MAT 會自動解析並生成報告,點擊 Dominator Tree,並按 Package分組,選擇自己所定義的 Package 類點右鍵,在彈出菜單中選擇 List objects->With incoming references。這時會列出所有可疑類,右鍵點擊某一項,並選擇 Path to GC Roots -> exclude weak/soft references,會進一步篩選出跟程序相關的所有有內存泄露的類。據此,可以追蹤到代碼中的某一個產生泄露的類。

MAT 的界面如下圖所示。



 

瞭解 MAT 中各個視圖的作用很重要,例如 www.eclipse.org/mat/about/screenshots.php 中介紹的。

總之使用 MAT 分析內存查找內存泄漏的根本思路,就是找到哪個類的對象的引用沒有被釋放,找到沒有被釋放的原因,也就可以很容易定位代碼中的哪些片段的邏輯有問題了。下一節將用一個示例來說明MAT詳細的使用過程。

2.3.3 MAT使用方法

1. 構建演示程序

首先需要構建一個演示程序,並獲取hprof文件。程序很簡單,按下Button後就循環地new自定義對象SomeObj,並將對象add到ArrayList中,直到拋出OutOfMemoryError,此時會捕獲該錯誤,同時使用android.os.Debug.dumpHprofData方法dump該進程的內存快照到/sdcard/oom.hprof文件中。

package com.demo.oom;

 

import java.io.IOException;

import java.util.ArrayList;

 

import android.app.Activity;

import android.os.Bundle;

import android.widget.Button;

import android.view.View;

 

publicclass OOMDemoActivity extends Activity implements View.OnClickListener {

privatestaticfinal String HPROF_FILE = "/sdcard/oom.hprof";

private Button mBtn;

private ArrayList<SomeObj> list = new ArrayList<SomeObj>();

 

@Override

publicvoid onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        mBtn = (Button)findViewById(R.id.btn);

        mBtn.setOnClickListener(this);

}

 

@Override

publicvoid onClick(View v) {

        try {

                while (true) {

                        list.add(new SomeObj());

                }

        catch (OutOfMemoryError e) {

                try {

                        android.os.Debug.dumpHprofData(HPROF_FILE);

                        throw e;

                catch (IOException e1) {

                        e1.printStackTrace();

                }

        }

}

 

        private class SomeObj {

                private static final intDATA_SIZE = 1 * 1024 * 1024;

                private byte[] data;

                SomeObj() {

                        data = newbyte[DATA_SIZE];

                }

        }

}

因爲要寫入SDCard,所以要在AndroidManifest.xml中聲明WRITE_EXTERNAL_STORAGE的權限。

注意:演示程序中是使用平臺API來獲取dump hprof文件的,你也可以使用ADT的DDMS工具來dump。每個hprof都是針對某一個Java進程的,如果你dump的是com.demo.oom進程的hprof,是無法用來分析system_server進程的內存情況的。

編譯並運行程序最終會在SDCard中生成oom.hprof文件,log中會打印相關的日誌信息,請留意紅色字體:

I/dalvikvm(1238): hprof: dumping heap strings to "/sdcard/oom.hprof".

I/dalvikvm(1238): hprof: heap dump completed (21354KB)(虛擬機dump了hprof文件)

D/dalvikvm(1238): GC_HPROF_DUMP_HEAP freed <1K, 13% free 20992K/23879K, external 716K/1038K, paused 4034ms

D/AndroidRuntime(1238): Shutting down VM

W/dalvikvm(1238): threadid=1: thread exiting with uncaught exception (group=0x40015560)

E/AndroidRuntime(1238): FATAL EXCEPTION: main

E/AndroidRuntime(1238): java.lang.OutOfMemoryError(是OOM錯誤)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity$SomeObj.<init>(OOMDemoActivity.java:45)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity.onClick(OOMDemoActivity.java:29)

E/AndroidRuntime(1238): at android.view.View.performClick(View.java:2485)

E/AndroidRuntime(1238): at android.view.View$PerformClick.run(View.java:9080)

E/AndroidRuntime(1238): at android.os.Handler.handleCallback(Handler.java:587)

E/AndroidRuntime(1238): at android.os.Handler.dispatchMessage(Handler.java:92)

E/AndroidRuntime(1238): at android.os.Looper.loop(Looper.java:123)

E/AndroidRuntime(1238): at android.app.ActivityThread.main(ActivityThread.java:3683)

(從方法堆棧可以看到是應用進程的主線程中發生了OOM

E/AndroidRuntime(1238): at java.lang.reflect.Method.invokeNative(Native Method)

E/AndroidRuntime(1238): at java.lang.reflect.Method.invoke(Method.java:507)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)

E/AndroidRuntime(1238): at dalvik.system.NativeStart.main(Native Method)

W/ActivityManager(61): Force finishing activity com.demo.oom/.OOMDemoActivity

D/dalvikvm(229): GC_EXPLICIT freed 8K, 55% free 2599K/5703K, external 716K/1038K, paused 1381ms

W/ActivityManager(61): Activity pause timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

W/ActivityManager(61): Activity destroy timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

I/Process(1238): Sending signal. PID: 1238 SIG: 9(錯誤沒有捕獲被拋給虛擬機,最終被kill掉)

I/ActivityManager(61): Process com.demo.oom (pid 1238) has died.(應用進程掛掉了)

獲取hprof文件後再用hprof-conv工具轉換一下格式:

D:\work\android\sdk\tools>hprof-conv.exe C:\Users\ray\Desktop\oom.hprof C:\Users

\ray\Desktop\oom\oom.hprof(將轉換後的hprof放到一個單獨的目錄下,因爲分析時會生成很多中間文件)

2. MAT提供的各種分析工具

使用MAT導入轉換後的hprof文件,導入時會讓你選擇報告類型,選擇“Leak Suspects Report”即可。然後就可以看到如下的初步分析報告:



 

MAT在Overview視圖中用餅圖展示了內存的使用情況,列出了佔用內存最大的Java對象com.demo.oom.OOMDemoActivity,我們可以根據這個線索來繼續調查,但如果沒有這樣的提示,也可以根據自己推斷來分析。在進一步分析之前,需要先熟悉MAT爲我們提供的各種工具。

(1) Histogram

列出每個類的實例對象的數量,是第一個非常有用的分析工具。



 

可以看到該視圖一共有四列,點擊列名可以按照不同的列以升序或降序排序。每一列的含義爲:

Class Name:類名

Objects:每一種類型的對象數量

Shallow Heap:一個對象本身(不包括該對象引用的其他對象)所佔用的內存

Retained Heap:一個對象本身,以及由該對象引用的其他對象的Shallow Heap的總和。官方文檔中解釋爲:Generally speaking, shallow heap of an object is its size in the heap and retained size of the same object is the amount of heap memory that will be freed when the object is garbage collected.

默認情況下該視圖是按照Class來分類的,也可以點擊工具欄中的選擇不同的分類類型,這樣可以更方便的篩選需要的信息。

默認情況下該視圖只是粗略的計算了每種類型所有對象的Retained Heap,如果要精確計算的話可以點擊工具欄中的來選擇。

有時爲了分析進程的內存使用情況,會對一個在不同的時間點抓取多個hprof文件來觀察,MAT提供了一個非常好的工具來對比這些hprof文件,點擊工具欄中的可以選擇已經打開的其他hprof文件,選擇後MAT將會對當前的hprof和要對比的hprof做一個插值,這樣就可以很方便的觀察對象的變化了。不過這個工具只有在Histogram視圖中才有。

列表的第一行是一個搜索框,可以輸入正則式或者數量來過濾列表的內容。



 

    1. (2) Dominator Tree

列出進程中所有的對象,是第二個非常有用的分析工具。



 

和Histogram不同的是左側列的是對象而不是類(每個對象還有內存地址,例如@0x40516b08),而且還多了Percentage一列。

右鍵點擊任意一個類型,會彈出一個上下文菜單:



 

菜單中有很多其他非常有用的功能,例如:

List Objects(with outgoing references/with incoming references):列出由該對象引用的其他對象/引用該對象的其他對象;

Open Source File:打開該對象的源碼文件;

Path To GC Roots:由當前對象到GC Roots引用鏈

GC Roots:A garbage collection root is an object that is accessible from outside the heap.也就是指那些不會被垃圾回收的對象。圖中標識有黃色圓點的對象就是GC Roots,每個GC Root之後都會有灰黑色的標識表明這個對象之所以是GC Root的原因。使得一個對象成爲GC Root的原因一般有以下幾個:

System Class

Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .

JNI Local

Local variable in native code, such as user defined JNI code or JVM internal code.

JNI Global

Global variable in native code, such as user defined JNI code or JVM internal code.

Thread Block

Object referred to from a currently active thread block.

Thread

A started, but not stopped, thread.

Busy Monitor

Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.

Java Local

Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

Native Stack

In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.

Finalizer

An object which is in a queue awaiting its finalizer to be run.

Unfinalized

An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.

Unreachable

An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.

Unknown

An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

在上圖的“Path To GC Roots”的菜單中可以選擇排除不同的非強引用組合來篩選到GC Roots的引用鏈,這樣就可以知道有哪些GC Roots直接或間接的強引用着當前對象,導致無法釋放了。



 

(3) Top Consumers

以class和package分類表示佔用內存比較多的對象。

(4) Leak Suspects

對內存泄露原因的簡單分析,列出了可能的懷疑對象,這些對象可以做爲分析的線索。

(5) OQL

MAT提供了一種叫做對象查詢語言(Object Query Language,OQL)的工具,方便用於按照自己的規則過濾對象數據。例如想查詢我的Activity的所有對象:

SELECT * FROM com.demo.oom.OOMDemoActivity

或者想查詢指定package下的所有對象:

SELECT * FROM “com.demo.oom.*” (如果使用通配符,需要用引號)

或者想查詢某一個類及其子類的所有對象:

SELECT * FROM INSTANCEOF android.app.Activity

還有很多高級的用法請參考幫助文檔。

3. 使用MAT分析OOM原因

熟悉了以上的各種工具,就可以來分析問題原因了。分析的思路有很多。

思路一:

首先我們從MAT的提示中得知com.demo.oom.OOMDemoActivity @ 0x40516b08對象佔用了非常多的內存(Shallow Size: 160 B Retained Size: 18 MB),我們可以在DominatorTree視圖中查找該對象,或者通過OQL直接查詢該類的對象。



 

按照Retained Heap降序排列,可以知道OOMDemoActivity對象之所以很大是因爲有一個佔用內存很大的ArrayList類型的成員變量,而根本原因是這個集合內包含了很多1MB以上的SomeObj對象。此時就可以查看代碼中對SomeObj的操作邏輯,查找爲什麼會有大量SomeObj存在,爲什麼每個SomeObj都很大。找到問題後想辦法解決,例如對SomeObj的存儲使用SoftReference,或者減小SomeObj的體積,或者發現是由於SomeObj沒有被正確的關閉/釋放,或者有其他static的變量引用這SomeObj。

思路二:

如果MAT沒能給出任何有價值的提示信息,我們可以根據自己的判斷來查找可以的對象。因爲發生OOM的進程是com.demo.oom,可以使用OQL列出該進程package的所有對象,然後再查找可疑的對象。對應用程序來說,這是非常常用的方法,如下圖。



 

通過查詢發現SomeObj的對象數量特別多,假設正常情況下對象用完後應該立即釋放纔對,是什麼導致這些對象沒有被釋放呢?通過“Path To GC Roots”的引用鏈可以知道是OOMDemoActivity中的list引用了SomeObj,所以可以考慮SomeObj是否錯誤的被添加進了list中,如下圖。



 

總之,分析的根本目的就是找到那些數量很大或者體積很大的對象,以及他們被什麼樣的GC Roots引用而沒有被釋放,然後再通過檢查代碼邏輯找到問題原因。

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