Android記憶體分析和調優(中)

yangxi_001發表於2013-12-05

前文中討論瞭如果使用adb shell procrank, dumpsys meminfo和showmaps分析程式的記憶體佔用情況。

本文將繼續細化,具體分析導致記憶體過大的dalvik heap。

Dalvik heap分析和優化

Dalkvik heap是最常見的android應用記憶體優化的物件。

通過上文的分析,我們可以通過adb shell的命令,知道用了多少dalvik heap。在ADT的eclipse的DDMS檢視,可以更細緻的檢視這些記憶體用到什麼地方。
參考DDMS使用說明(搜尋viewing heap),我們可以首先在devices view中選中一個程式,然後enable "update heap“(不帶紅箭頭的半杯水圖示),之後在heap view中點選”Cause GC"。這樣子除了Heap Size, Allocated, Freed,還可以看到data object,class object,和n-byte array分別佔用的記憶體大小。

不過真心說,這個還是太粗糙了,沒法精確到具體的類。此時大名鼎鼎的MAT就派上用場了。

MAT是對java記憶體映象進行分析的工具。所以首先需要匯出程式的記憶體映象,可以在DDMS上的device view點選Dump HPROF file(帶紅箭頭的半杯水圖示),生成hprof檔案。因為android的檔案格式跟通用的java的hprof格式不一樣,還需要通過hprof-conv命令來轉換。然後就可以用MAT來開啟。

看起來挺麻煩的。事實上,現在MAT的eclipse外掛可以把上面的工具一鍵完成。只需要點選Dump HPROF file圖示,然後MAT外掛就會自動轉換格式,並且在eclipse中開啟分析結果。eclipse中還專門有個Memory Analysis檢視,可以更詳細的檢視MAT的分析結果。

MAT可以根據記憶體映象,以視覺化的方式告訴我們哪個類,哪個物件分配了多少記憶體。但如果只是這樣,用處就沒那麼大了。因為不像c++的物件本身可以存放大量記憶體,java的物件成員都是些引用。真正的記憶體都在堆上,看起來是一堆原生的byte[], char[], int[]。所以我們如果只看物件本身的記憶體,那麼數量都很小。我們稱之位shallow heap。

於是MAT提出了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來選擇這個組。(又開始Histogram中不顯示Retained heap,需要點選那個計算器的按鈕才會計算出來)。這裡最小的粒度是類級別的。

為了計算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。這是個真正的引用圖的概念,表示該物件的出節點(被該物件引用的物件)和入節點(引用到該物件的物件)。

另外一個類似的功能是右鍵選單的Path to GC RootsGC roots是可能導致GC的節點。這個Path則是從這些GC root節點中的某個到當前物件的最短引用路徑。對這個如何計算不是很確定,我想應該是根據引用樹而不是dominator tree。後面會看到這個功能在非常的有用。

說完工具,下面是具體的減少記憶體大小。一般要解決兩個問題:記憶體洩露和釋放暫時不需要的記憶體。

Java記憶體洩露歸根結底都是一個原因導致的,應該被釋放的物件被生命期更長的物件引用,所以沒法被GC。這個生命期更長的物件很常見的是static物件,會持續整個程式。在個人實際工作中,我會先用adb shell dumpsys meminfo檢視dalvik heap會不會持續增長。如果是,我會在在dominator Tree中按照Retained Memory排序,找出比較大的(經常是Bitmap),然後用Path to GC Roots看看其引用情況。在這個Path中,一般會發現我們app自己包的類,可以分析這個類是不是還是需要的。如果不需要,那說明可能存在記憶體洩露。此時,在對這個自己包的類檢視incoming references。看看到底是哪些引用導致它沒有釋放。用這種方法,會比較快的發現問題。MAT自己也提供了智慧的記憶體分析工具,我沒有用,不好評論。

一個製造記憶體洩露的很有效的辦法是不斷的切換橫屏和豎屏。現實中很多記憶體洩露都是因為static的物件指向了Activity物件(作為context傳),而切換橫屏和豎屏會導致Activity重新生成。所以如果有問題,記憶體很快就會變大。從編碼上講,avoid-memory-leak這篇文章教育我們,在需要context的地方,儘量使用getApplicationContext,而不是Activity本身。

另外一個可以減少記憶體的方法是刪除臨時不用的記憶體。編碼中可能是為了記憶體cache以提高效能,可能只是偷懶,之前場景使用的記憶體並沒有被釋放掉。這樣子下次再回到這個場景,會快一點;但會可能會佔用不少記憶體。我覺得在android這類記憶體受限的系統上,還是應該謹慎使用控制元件換時間的策略。如果想刪除臨時不用的記憶體,也可以使用mat像監測記憶體洩露一樣,看看哪些比較大的記憶體臨時不用卻仍然被引用,然後刪除對其引用。

關於mat的一個小技巧是mat經常發現比較大的記憶體洩露是圖片,此時如果知道圖片是什麼內容就很容易定位到何時導致的記憶體洩露。這個帖子回答了這個問題。

關於dalvik mat最後再推薦自己看的一個android memory manage video(slides , contentcontent2)。裡面對MAT和記憶體洩露都有介紹。這個blog也是對二者都有介紹,很好。關於MAT更好的文件集合在這裡,MAT作者寫的。

相關文章