ANDROID 探究oom內幕

從早期G1的192MB RAM開始,到現在動輒1G -2G RAM的設備,爲單個App分配的內存從16MB到48MB甚至更多,但OOM從不曾離我們遠去。這是因爲大部分App中圖片內容佔據了50%甚至75%以上,而App內容的極大豐富,所需的圖片越來越多,屏幕尺寸也越來越大分辨率也越來越高,所需的圖片的大小也跟着往上漲,這在大屏手機和平板上尤其明顯。而且還經常要兼容低版本的設備。所以Android的內存管理顯得極爲重要。



在這裏我們主要講兩件事情:
1.Gingerbread和Honeycomb中的一些影響你使用內存的變化
-heap size
-GC
-bitmaps
2.理解heap的用途分配
-logs
-merory leaks
-Eclispe Memory Analyzer(MAT)



首先第一部分,我們都知道Android是個多任務操作系統,同時運行着很多程序,都需要分配內存,不可能爲一個程序分配越來越多的內存以至於讓整個系統都崩潰,因此heap的大小有個硬性的限制,跟設備相關,從發展來說也是越來越大,G1:16MB,Droid:24MB,Nexus One:32MB,Xoom:48MB,但是一旦超出了這個使用的範圍,OOM便產生了。如果你正在開發一個應用,想知道設備的heap大小的限制是多少,比方說根據這個值來估算自己應用的緩存大小應該限制在什麼樣一個水平,你可以使用ActivityManager.getMemoryClass ()來獲得一個單位爲MB的整數值,一般來說最低不少於16MB,對於現在的設備而言這個值會越來越大,24MB,32MB,48MB甚至更大。



但是對於一些內存非常吃緊的比如圖片瀏覽器等應用,在平板上所需的內存更大了。因此在Honeycomb之後AndroidManifest.xml增加了largeHeap的選項



1
2
3
4
<application
       android:largeHeap="true"
       ...
</application>



這允許你的應用使用更多的heap,可以用ActivityManager.getLargeMemoryClass ()返回一個更大的可用heap size。但是這裏要警告的是,千萬不要因爲你的應用報OOM了而使用這個選項,因爲更大的heap size意味着更多的GC時間,意味着應用的性能越來越差,而且用戶也會發現其他應用很有可能會內存不足。只有你需要使用很多的內存而且非常瞭解每一部分內存的用途,這些所需的內存都是不可或缺的,這個時候你才應該使用這個選項。



剛剛我們提到更大的heap size意味着更多的GC時間,下面我們來談談Garbage Collection。



1.jpg



如上圖所示,GC會選擇一些它瞭解還存活的對象作爲內存遍歷的根節點,比方說thread stack中的變量,JNI中的全局變量,zygote中的對象等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。如下圖藍色部分。



2.jpg



因此也可以看出,更大的heap size需要遍歷的對象更多,回收垃圾的時間更長,所以說使用largeHeap選項會導致更多的GC時間。



在Gingerbread之前,GC執行的時候整個應用會暫停下來執行全面的垃圾回收,因此有時候會看到應用卡頓的時間比較長,一般來說>100ms,對用戶而言已經足以察覺出來。Gingerbread及以上的版本,GC做了很大的改進,基本上可以說是併發的執行,也不是執行完全的回收,只有在GC開始以及結束的時候會有非常短暫的停頓時間,一般來說<5ms,用戶也不會察覺到。



在Honeycomb之前,Bitmap的內存分配如下圖。



3.jpg



藍色部分是Dalvik heap,黃色部分是Bitmap引用對象的堆內存,而Bitmap實際的Pixel Data是分配在Native Memory中。這樣做有幾個問題,首先需要調用reclyce()來表明Bitmap的Pixel Data佔用的內存可回收,不調用這個方法的話就要靠finalizer來讓GC回收這部分內存,但瞭解finalizer的應該都知道這相當的不可靠;其次是很難進行Debug,因爲一些內存分析工具是查看不到Native Memory的;再次就是不調用reclyce()需要回收Native Memory中的內存的話會導致一次完整的GC,GC執行的時候會暫停整個應用。



Honeycomb之後,Bitmap的內存分配做出了改變,如下圖



4.jpg



藍色黃色部分沒有變化,但Bitmap實際的Pixel Data的內存也同樣分配在了Dalvik heap中。這樣做有幾個好處。首先能同步的被GC回收掉;其次Debug變得容易了,因爲內存分析工具能夠查看到這部分的內存;再次就是GC變成併發了,可做部分的回收,也就是極大縮短了GC執行時暫停的時間。



接下來我們講第二部分。一般來說我們希望瞭解我們應用內存分配,最基本的就是查看Log信息。比方說看這樣一個Log信息(這是Gingerbread版本的,Honeycomb以後log信息有改動):



D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/
9991K, external 4703K/5261K, paused 2ms 2ms



GC_XXX表明是哪類GC以及觸發GC的原因。幾種GC類型:
- GC_CONCURRENT:這是因爲你的heap內存佔用開始往上漲了,爲了避免heap內存滿了而觸發執行的。
- GC_FOR_MALLOC:這是由於concurrent gc沒有及時執行完而你的應用又需要分配更多的內存,內存要滿了,這個時候不得不停下來進行malloc gc。
- GC_EXTERNAL_ALLOC:這是爲external分配的內存執行的GC,也就是上文提到的Bitmap Pixel Data之類的。
- GC_HPROF_DUMP_HEAP:這是當你做HPROF這樣一個操作去創建一個HPROF profile的時候執行的。
- GC_EXPLICIT:這是由於你顯式的調用了System.gc(),這是不提倡的,一般來說我們可以信任系統的GC。



freed 2049K表明在這次GC中回收了多少內存。
65% free 3571K/9991K是heap的一些統計數據,表明這次回收後65%的heap可用,存活的對象大小3571K,heap大小是9991K。
external 4703K/5261K是Native Memory的數據。放Bitmap Pixel Data或者是NIO Direct Buffer之類的。第一個數字表明Native Memory中已分配了多少內存,第二個值有點類似一個浮動的閥值,表明分配內存達到這個值系統就會觸發一次GC進行內存回收。
paused 2ms 2ms表明GC暫停的時間。從這裏你可以看到越大的heap size你需要暫停的時間越長。如果是concurrent gc你會看到2個時間一個開始一個結束,這時間是很短的,但如果是其他類型的GC,你很可能只會看到一個時間,而這個時間是相對比較長的。



通過Log可以對內存信息有個基本的瞭解,但這不足以瞭解什麼對象在使用內存,在哪使用了內存。這時候你需要用Heap Dumps。一個Heap Dump基本上來說就是一個包含你heap中所有對象信息的二進制文件。你可以用DDMS來生成這個文件,點擊DDMS中下圖的那個按鈕。



5.jpg



同時Heap Dumps也有對應的API,你想要在特定的時間點獲取一份Heap Dump,使用android.os.Debug.dumpHprofData()。獲取到的文件要轉換成標準的HPROF格式,使用如下命令:hprof-conv orig.hprof converted.hprof。然後用MAT或者jhat進行分析。



在講MAT之前先講下Memory Leaks。要清楚GC並不能防止Memory Leaks,所謂Memory Leaks就是引用到了已經沒用的對象從而讓這些對象避免了被GC回收,跟C/C++中的概念並不一樣。容易導致內存泄漏的是一些Activity,Context,View,Drawable之類的引用,和一些非靜態的內部類比方說Runnable之類的以及一些Caches。比如你旋轉屏幕的時候在新的方向上產生一個新的Activity,如果有變量引用到舊的Activity就會導致其無法被GC,造成Memory Leaks。



通常通過上面介紹的Log信息,只要已用memory一直處於上升的情形而不回落,便大致能瞭解到應用存在Memory Leaks。不過MAT這類工具可以幫助你更好的對memory進行分析。使用MAT之前有2個概念是要掌握的:Shallow heap和Retained heap。Shallow heap表示對象本身所佔內存大小,一個內存大小100bytes的對象Shallow heap就是100bytes。Retained heap表示通過回收這一個對象總共能回收的內存,比方說一個100bytes的對象還直接或者間接地持有了另外3個100bytes的對象引用,回收這個對象的時候如果另外3個對象沒有其他引用也能被回收掉的時候,Retained heap就是400bytes。



MAT使用Dominator Tree這樣一種來自圖形理論的概念。



6.jpg



所謂Dominator,就是Flow Graph中從源節點出發到某個節點的的必經節點。那麼根據這個概念我們可以從上圖左側的Flow Graph構造出右側的Dominator Tree。這樣一來很容易就看出每個節點的Retained heap了。Shallow heap和Retained heap在MAT中是非常有用的概念,用於內存泄漏的分析。



我們用Honeycomb3.0中的HoneycombGallery做一個Demo。在工程的MainActivity當中加入如下代碼:



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class MainActivity extends Activity implements ActionBar.TabListener {
    static Leaky leak = null;
    class Leaky {
        void doSomething() {
            System.out.println("Wheee!!!");
        }
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (leak == null) {
            leak = new Leaky();
        }
        ...



上面這段代碼,對Java熟悉的同學都應該瞭解內部類對象持有了外部類對象引用,而leak作爲靜態變量在非空判斷下只產生了一個對象,因此當旋轉屏幕時生成新的Activity的時候舊的Activity的引用依然被持有,如下圖:



8.jpg



通過觀察旋轉屏幕前後Log中GC的信息也能看出heap的分配往上漲了許多,並且在GC執行完heap的分配穩定之後並沒有降下來,這就是內存泄漏的跡象。



我們通過MAT來進行分析。先下載MAT,可以作爲Eclipse插件下載,也可以作爲RCP應用下載,本質上沒有區別。DDMS中選中應用對應的進程名,點擊Dump HPROF file的按鈕,等一小段時間生成HPROF文件,如果是Eclipse插件的話,Eclipse會爲這個HPROF自動轉化成標準的HPROF並自動打開MAT分析界面。如果是作爲RCP應用的話,需要用sdk目錄tools中的hprof-conv工具來進行轉化,也就是上文提及的命令hprof-conv orig.hprof converted.hprof,這種方式保存HPROF文件的位置選擇更爲自主,你也可以修改Eclipse的設置讓Eclipse提示保存而不是自動打開,在Preferences -> Android -> DDMS中的HPROF Action由Open in Eclipse改爲Save to disk。打開MAT,選擇轉化好的HPROF文件,可以看到Overview的界面如下圖:



7.jpg



中間的餅狀圖就是根據我們上文所說的Retained heap的概念得到的內存中一些Retained Size最大的對象。點擊餅狀圖能看到這些對象類型,但對內存泄漏的分析還遠遠不夠。再看下方Action中有Dominator Tree和Histogram的選項,這一般來說是最有用的工具。還記得我們上文說過的Dominator Tree的概念嗎,這就是我們用來跟蹤內存泄漏的方式。點開Dominator Tree,會看到以Retained heap排序的一系列對象,如下圖:



9.png



Resources類型對象由於一般是系統用於加載資源的,所以Retained heap較大是個比較正常的情況。但我們注意到下面的Bitmap類型對象的Retained heap也很大,很有可能是由於內存泄漏造成的。所以我們右鍵點擊這行,選擇Path To GC Roots ->exclude weak references,可以看到下圖的情形:



10.png



Bitmap最終被leak引用到,這應該是一種不正常的現象,內存泄漏很可能就在這裏了。MAT不會告訴哪裏是內存泄漏,需要你自行分析,由於這是Demo,是我們特意造成的內存泄漏,因此比較容易就能看出來,真實的應用場景可能需要你仔細的進行分析。



根據我們上文介紹的Dominator的概念,leak對象是該Bitmap對象的Dominator,應該出現在Dominator Tree視圖裏面,但實際上卻沒有。這是由於MAT並沒有對weak references做區別對待,這也是我們選擇exclude weak references的原因。如果我們Path To GC Roots ->with all references,我們可以看到下圖的情形:



11.png



可以看到還有另外一個對象在引用着這個Bitmap對象,瞭解weak references的同學應該知道GC是如何處理weak references,因此在內存泄漏分析的時候我們可以把weak references排除掉。



有些同學可能希望根據某種類型的對象個數來分析內存泄漏。我們在Overview視圖中選擇Actions -> Histogram,可以看到類似下圖的情形:



12.png



上圖展示了內存中各種類型的對象個數和Shallow heap,我們看到byte[]佔用Shallow heap最多,那是因爲Honeycomb之後Bitmap Pixel Data的內存分配在Dalvik heap中。右鍵選中byte[]數組,選擇List Objects -> with incoming references,可以看到byte[]具體的對象列表:



13.png
14.png



我們發現第二個byte[]的Retained heap較大,內存泄漏的可能性較大,因此右鍵選中這行,Path To GC Roots -> exclude weak references,同樣可以看到上文所提到的情況,我們的Bitmap對象被leak所引用到,這裏存在着內存泄漏。



15.png



在Histogram視圖中第一行<Regex>中輸入com.example.android.hcgallery,過濾出我們自己應用中的類型,如下圖:



16.png



我們發現本應該只有一個MainActivity現在卻有兩個,顯然不正常。右鍵選擇List Objects -> with incoming references,可以看到這兩個具體的MainActivity對象。右鍵選中Retained heap較大的MainActivity,Path To GC Roots -> exclude weak references,再一次可疑對象又指向了leak對象。



17.png
18.png
19.png



以上是MAT一些基本的用法,如果你感興趣,可以自行深入的去了解MAT的其他功能

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