Wednesday, November 2, 2011

使用MAT(記憶體分析)工具查看Memory Leak

通常我們都認為JAVA有Garbage回收機制,
因此不太去理會記憶體的問題。
但如果今天要開發的程式用到大量(大張)的圖片,
因為Android手機的記憶體有限,
程式設計師最好還是要知道一下Memory Leak(記憶體洩漏)。

JAVA有參照(reference)這個特性,
參照如果沒有小心使用,
像︰
Bitmap使用後沒有recycle()、
Drawable使用後沒有setCallback(null)⋯
都會容易出現OutOfMemoryException而導致程式出錯。

Romain Guy在2009年和2010年,
曾藉由數個presentation裡表述如何檢查Memory Leak和防止Memory Leak所造成的OutOfMemory問題。

Android提供的DDMS就是一個這樣的好工具,
可以監看或倒出Memory的使用狀況。

文章開始前,你需要︰
2.設定好Android的環境變數︰Ubuntu的設定方式/Mac的設定方式
3.Eclipse要安裝一個外掛︰
MAT,全名Memory Analyzer Tool
它能將Memory Leak以圖形化的介面方式表現出來。

文章開始︰
MAT,Memory Analyzer Tool,
一個Eclipse上好用的記憶體監看套件。

在MAT裡,
我們很容易看到兩個值︰
Shallow Heap和Retained Heap。
這兩個專業名詞,
淘寶JAVA中間件團隊成都心情的部落格裡,
可以看到詳細的說明。
就我的理解,
Shallow Heap指的是物件本身的記憶體使用量
而Retained Heap指物件本身+參照過來的物件們所使用的記憶體使用量。

為了讓大家能更了解如何使用MAT,
這裡提供一個範例

一、安裝範例
這個範例是一個zip檔,
請用Eclipse[File]-->[Import]將檔案匯入
  
匯入後,
先將TestBitmapLeakAcitivity.java裡onDestroy()裡的removeBitmap()註解掉,
目的是讓程式在離開時,不要回收Bitmap
然後在專案上點擊滑鼠右鍵,啟動本專案































啟動後,
會看到一個小訊息︰[onCreate() Memory used: 2MB]
那個小訊息是告訴我們程式剛啟動時,
目前所使用的記憶體量。

再看一下畫面,還發現畫面中有一個add bitmap按鈕。
我設計每點擊按鈕就會在畫面增加一張圖片(記憶體也會往上累積)。
大概點個5次左右,
會發現記憶體不斷的在累加。
按[實機的返回鍵]關閉程式再進來一次,
訊息顯示︰[onCreate() Memory used: 9MB]
記憶體完全沒有因為之前的程式關閉而釋放,
反而繼續累加中。
這時候我們幾乎可以斷定,
這隻程式發生Memory Leak(記憶體洩漏)了!
我們需要查看Memory Leak發生在什麼地方。


二、查看Memory Leak
1.我們開啟DDMS,
並照以下的步驟,將記憶體快照(snapshot)取出
然後存檔

2.編譯此檔讓MAT工具能夠辨識
指令是︰
$hprof-conv /路徑/com.testMemoryLeak.hprof /路徑/leak.hprof
如︰
$hprof-conv /Users/simon/Desktop/untitled\ folder/com.testMemoryLeak.hprof /Users/simon/Desktop/untitled\ folder/leak.hprof

就會在目錄裡看到編譯好的leak.hprof檔

註︰
1.建議最好將編譯好的leak.hprof檔存在一個目錄裡面,
因為之後MAT在開啟此檔時,會製造出一堆檔案,
造成你的桌面大亂
2.如果沒有將Android的環境變數設定好,
會無法使用hprof-conv工具!

3.使用MAT工具開啟leak.hprof檔

這時候出現一個精靈詢問視窗,
點擊Leak Suspects Report,
產生Leak報表。
報表出來後,
看見一個圓餅圖,
清楚的告訴我們哪裡產生Memory Leak。

我們看到有4個可疑點造成Memory Leak(灰色不算在內)
將畫面往下拉,發現Class蠻可疑的(因為圓餅圖的區塊最大)












我們點擊下方的Detail
出現了更詳細的表格說明
 
點擊連結,選擇List objects-->with incoming references

藉由這個動作,我們知道Android在Runtime時啟動的Class Loader,
有哪些Class被指派(reference)到。
這個圖表描述了這段期間內的記憶體快照,
TestBitmapLeakActivity有2組資料,
其中有一組還擁有大量的Retained Heap。
因此我們從這一組含大量Retained Heap的資料往內追蹤。


我們快速的將這個含Retained Heap=48的數據點擊兩下。
發現到每筆程式所產出的物件,
幾乎都有參照(reference)到mContext。
這在Garbage回收是沒有問題的。

所以我們必須把懷疑的方向往別的地方推進。
我們一筆一筆的點開來檢查。
發現到有3筆ImageView,當中都有一個系統產生的物件BitmapDrawable參照到mCallback。
因為這個參照,讓程式關閉時,
無法完全被回收!

這表示︰Memory Leak罪証成立!還有未被回收遺留的Context。

註︰關於mCallback為什麼會是我們判斷的依據,
請看這篇

因此,我們得知原來造成關閉的程式無法被回收的原因是︰
ImageView沒有完全釋放掉

怎麼釋放掉ImageView使用的資源?
啟動bitmap回收機制,
反註解剛才讓大家註解的removeBitmap(),
然後重新執行一次程式並新增數張bitmap。

接著,按下[實機的返回鍵],
再次進入程式,
我們發現小訊息[onCreate() Memory used]的值不像之前一樣繼續累積,
代表前一次開啟程式的相關物件已經被回收掉了。

後記︰

1.我們還可以用交叉比對的方式,
檢查出Memory Leak前後數據的不同。

我們將程式的removeBitmap()反註解(啟動回收機制),
做出一張圓餅圖表。
再將程式的removeBitmap()註解(不啟動回收機制),
做出另一張圖餅圖表。

從下面圖示可以清楚的看到,
Memory Leak和沒有Memory Leak的狀況之不同。

沒有回收-->造成Memory Leak︰

有回收︰


2.經由以上的操作,
我們發現︰
MAT工具不會直接告訴我們哪裡產生Memory Leak,
但會乖乖的列出Dalvik產出了多少物件實體,
藉由程式設計師的開發經驗,
我們因而能判斷出造成Memory Leak的地方。

判斷Memory Leak的方式和邏輯還有很多種(官方布落格也教一種),
大家可以多爬文做功課。

此篇文章是研究了數天Memory Leak的心得,
若有錯誤煩請指正,
謝謝大家。

最後附上Google I/O 2011的MAT教學,
裡面有提到另一種使用MAT檢查Memory Leak的方法。
相信看完後會更明白Memory Leak和MAT工具。


參考文章︰
1.J2ME GAME
2.何謂Memory Leak(記憶體洩漏)
2.Memory Leak經驗分享-Drawable篇
3.記憶體超出OutOfMemoryError
4.使用JDK工具检查运行系统是否存在内存泄露
5.Android官方布落格2011/03/24
6.Android官方文件
7.直接使用程式Dump hprof檔



3 comments:

Anonymous said...

謝謝,正煩惱MAT不會用,不知道怎麼辦?剛好看到您的文章,很實用,給支持拉!

Unknown said...

其實我也是一直爬文,努力學當中。
如果你學會什麼新東西歡迎分享喔!
大家一起學習速度一定會比一個人學還快!
加油!

Kanzo said...

真是佛心來了的好文章