1. 程式人生 > >記憶體洩漏檢測分析工具MAT(Memory Analyzer Tool)的使用

記憶體洩漏檢測分析工具MAT(Memory Analyzer Tool)的使用

工具下載地址:https://www.eclipse.org/mat/

  首先準備一個案例demo。上次講了經典Handler導致的記憶體洩漏,今天也講個經典例子。單例造成的記憶體洩漏。

public class MySingleton {

    private static volatile MySingleton instance;

    private Context mContext;

    private MySingleton(Context context) {
        this.mContext = context;
    }

    public static MySingleton getInstance(Context context) {
        if (instance == null) {
            synchronized (MySingleton.class) {
                if (instance == null) {
                    instance = new MySingleton(context);
                }
            }
        }
        return instance;
    }

}

使用時如果傳的Activity的當前例項this進去,一旦Activity關閉後,單例仍然持有這個Activity,就會造成記憶體洩漏。正確的方法應該傳getApplicationContext()這個Context。

public class DemoActivity extends AppCompatActivity {

    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //塞一張大一點的圖片,用來增大Activity的所需記憶體,可以更好的在MAT中觀測
        iv = new ImageView(this);
        iv.setImageResource(R.drawable.demo);

        MySingleton.getInstance(this);
    }

}


開啟Android Minitor點選裡面的Dump Java Heap可以生成一個hprof檔案,字尾名是heap profile的縮寫。這類檔案統一放在左側的Captures窗口裡。

這檔案在MAT不能直接開啟,所以需要在這裡右鍵這個檔案,選擇Export to standard .hprof轉成成標準格式。

先講兩個概念

關於Shallow Size和Retained Size

https://www.yourkit.com/docs/java/help/sizes.jsp 或這篇譯文 http://blog.csdn.net/kingzone_2008/article/details/9083327 講的很詳細.總結就是:

Shallow Size是物件本身佔據的記憶體的大小,不包含其引用的物件。對於常規物件(非陣列)的Shallow Size由其成員變數的數量和型別來定,陣列的ShallowSize由陣列型別和陣列長度來決定,它為陣列元素大小的總和。
Retained Size是物件本身,加上可直接或間接引用到的物件的大小,其中要減去被GC Roots存在另外一條路徑引用的物件。所以這也可以理解為GC之後所能回收到記憶體的總和。

關於GC Root

Java GC是自動執行自我管理的,垃圾回收當然要判斷出哪些是已經不會再使用需要回收的物件。這怎麼判斷?就是設立一個GC Root的概念,它表示根引用集合。從這個GC Root無法達到的物件,就是沒有辦法再獲取和使用到,也就是需要回收的。

GC Roots具體有哪些東西可以參考http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3

記不住那麼多沒關係,主要記住有“所有執行緒中正在執行的方法棧裡指向到堆裡的物件的那個引用”,“所有靜態引用變數”,“所有引用型別常量”,“所有CalssLoader”這3個就行。

回到MAT使用,一般有這麼幾個用法。

Histogram

裡面會根據類名來區分列舉出表格,後面3個列是型別的物件個數,Shallow Size和Retained Size。預設是按照Shallow Size排序,也可以更改排序方式。

Dominator Tree

裡面是根據物件來區分列舉出表格,後面3個列是Shallow Size,Retained Size和佔據的百分比。預設是按Retained Size排序。

都說是Dominator Tree,中文翻譯為支配(物件)樹?所以點選每一行左邊的箭頭會列舉出這個物件所支配的所有物件。

注意是支配(物件),而不是用持有(物件)樹。下面會講這兩者的區別。

可以看到這些表格視窗都可以在列表上方的搜尋欄上輸入關鍵字搜尋,支援正則表示式。

List Object

在上面的表格裡,對任一行右鍵可以呼叫List Object功能。兩個子選項人如其名:

with outgoing references可以列出這個物件裡所持有的所有物件們。

with incoming references可以列出所有持有這個物件的物件們。

Dominator Tree和With outgoing references的區別

持有就是我們一般講的該物件持有一個另一個物件的引用。支配的意思不侷限於直接持有或間接持有,而是突出一種唯一性的特點。比如A支配B,表示要經過B的引用路徑裡必須經過A。

關於支配的概念以下這篇文章會講的很詳細 http://book.51cto.com/art/201504/472215.htm

下面列舉DemoActivity的Dominator Tree和with outgoing references兩個視窗對比一下。

可以發現,明顯outgoing這個視窗會有比較多的物件。看了一遍,剛好左邊有的物件右邊都有,說明沒有間接引用卻達成支配的情況。

Path To GC Roots

首先說明Path To GC Roots是針對Dominator Tree表格的。在Histogram裡只有Merge shortest Path To GC Roots,意思是會找出最短的路徑。

Path To GC Roots人如其名,意為顯示出GC Roots到這個物件的所有路徑。一般用exclude all phantom/weak/soft etc. references這個排除虛、弱、軟引用的這個選項。因為這幾種情況堆記憶體GC都可以回收的到,不是造成記憶體洩漏的原因。唯有強引用指向的物件GC回收不了。選中後出現如下圖表格

這圖很直白。DemoActivity物件 ←(被持有) MySingleton物件的mContext引用 ←(被持有) MySingleton物件的instance引用(因為是靜態變數,GC不會回收)。

使用Compare Basket面板比較兩個hprof的histogram檔案

這裡我使用了Handler導致記憶體洩漏的例子。Before.hprof表示使用了非靜態內部類,After.hprof表示使用了靜態內部類。生成hprof檔案前一定要先執行Initiate GC。

在選單欄裡選擇“Window”→“Navigation History”,裡面能顯示當前hprof檔案開啟的幾個頁面。

可以對裡面的“histogram”、“dominator_tree”、“OQL執行結果”右鍵,選擇Add to Compare Basket

比如我利用了OQL語句把Before和After的所有Activity物件搜尋結果都加入了Compare Basket。

在Compare Basket裡,選中兩個檔案,可以點選下面的紅色感嘆號進行對比。也可以右鍵後選擇compare Tables,支援自定義對比引數。

對比結果出來

我App使用過程是從MainActivity中打開了3次含有Handler的SampleActivity然後退出。沒有使用靜態內部類的那個Before.hprof導致記憶體中依然存在了3個SampleActivity例項。

Leak Suspects和Top Consumers

Overview頁面裡的Reports標籤裡,提供了Leak Suspects和Top Consumers兩種報告。

Leak Suspects提供了MAT工具猜測了一些記憶體洩漏的地方,會生成直觀的餅狀圖。也可以方便的點選檢視對應的引用路徑和支配樹結構。

Top Consumers分別提供了佔用記憶體最大的幾個物件,幾種物件型別,幾個classloader,幾個包的餅狀圖和相應的詳細資料表格。

然而在Android應用中,一般都會被BitmapDrawable$BitmapState這種垃圾資訊佔用著。所以一般我都不會用這兩個工具。

該如何正確的查詢記憶體洩漏

方法有很多種,這裡列舉幾個我常用的例子

1.檢視當前開發App所有物件的記憶體情況

可以使用Group by package功能

也可以在搜尋欄裡寫出包名com.yao.memorytest

2.檢視Activity的記憶體洩漏

輸入正則表示式com\.yao\.memorytest\.[a-zA-Z0-9_]+Activity。它是由包名開頭,Activity結尾組成。專注搜尋Activity物件。(會把Activity的內部類也查出來,這個正則表示式加結束符$會失效)

3.Object Query Language

OQL類似於SQL,O代表Object物件的意思嘛,S代表.....(媽的不穀歌一下還真忘了S代表什麼意思)

輸入SELECT * FROM "com.yao.memorytest.*Activity"搜尋出來的結果比正則給力,不會把內部類也查出來了。

或者輸入SELECT * FROM INSTANCEOF android.app.Activity 把java語法裡面instanceof Activity的物件搜尋出來。

更多OQL語法可以谷歌。

以上3方法得到表格後,只能根據自身App執行情況和業務邏輯分析,加上檢視Retained Heap大小的排序逐個排查了。

還有一個給力的記憶體洩漏檢測工具叫LeakCanary,可以參考連結LeakCanary 中文使用說明

常見的記憶體洩漏

1.Handler導致的記憶體洩漏,詳情看我之前文章。

2.使用靜態引用變數,然後這個物件又剛好持有Activity這類例項的。

3.單例需要Context時,沒使用全域性的ApplicationContext,而是用的Activity,Service這些Context。

4.File,資料庫,IO,還有廣播,eventbus,一些Listener等這些需要成對程式設計的,只寫了開啟沒寫關閉。

參考

http://wiki.eclipse.org/MemoryAnalyzer