Android內存分析和調優(中)


前文中討論了如果使用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作者寫的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM