Android內存分析之MAT

  面試中經常會問到內存優化,我們在開發過程中也多少會遇到OOM的問題,根據大牛們的博客,記錄下我的學習思路

一、爲何會OOM?

1. 一直以來Andorid手機的內存都比iPhone(iPhone6RAM1G)大的多,Android卻經常出現OOM,這是爲何?

 這個是因爲Android系統對dalvik的vm heapsize 作了硬性限制,當java進程申請的java空間超過閥值時,就會拋出OOM異常(這個閥值可以是48M、24M、16M等,視機型而定),可以通過adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。(我的一加2 Android6.0.1已經達到了256M)

 也就是說,程序發生OOM並不表示RAM不足,而是因爲程序申請的java heap對象超過了dalvik.vm.heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM

 這樣的設計似乎有些不合理,但是Google爲什麼這樣做呢?這樣設計的目的是爲了讓Android系統能同時讓比較多的進程常駐內存,這樣程序啓動時就不用每次都重新加載到內存,能夠給用戶更快的響應。迫使每個應用程序使用較小的內存,移動設備非常有限的RAM就能使比較多的app常駐其中。

2. 大型遊戲如何在較小的heapsize上運行?

  • 創建子進程

    創建一個新的進程,那麼我們就可以把一些對象分配到新進程的heap上了,從而達到一個應用程序使用更多的內存的目的,當然,創建子進程會增加系統開銷,而且並不是所有應用程序都適合這樣做,視需求而定。

    創建子進程的方法:使用android:process標籤

  • 使用jni在native heap上申請空間(推薦使用)

    nativeheap的增長並不受dalvik vm heapsize的限制,從圖6可以看出這一點,它的native heap size已經遠遠超過了dalvik heap size的限制。

    只要RAM有剩餘空間,程序員可以一直在native heap上申請空間,當然如果 RAM快耗盡,memory killer會殺進程釋放RAM。大家使用一些軟件時,有時候會閃退,就可能是軟件在native層申請了比較多的內存導致的。比如,我(餘龍飛)就碰到過UC web在瀏覽內容比較多的網頁時閃退,原因就是其native heap增長到比較大的值,佔用了大量的RAM,被memory killer殺掉了。(Fresco使用的就是這種方式)

  • 使用顯存(操作系統預留RAM的一部分作爲顯存)

    使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,這個我沒有實踐過。再比如Android中GraphicBufferAllocator申請的內存就是顯存。

3. Android內存究竟如何?(native heap、java heap)

  • 進程的地址空間

    在32位操作系統中(Native Process),進程的地址空間爲0到4GB:

    圖一

    1. kernel space(內河空間):這些地址用戶代碼不能讀也不能寫
    2. Memory Mapping Segment(內存映射):段文件映射(包括動態庫)和匿名映射。
    3. Stack:(進棧和出棧)由操作系統控制,其中主要存儲函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般爲幾MB大小。
    4. Heap:空間的使用由程序員控制,程序員可以使用malloc、new、free、delete等函數調用來操作這片地址空間。Heap爲程序完成各種複雜任務提供內存空間,所以空間比較大,一般爲幾百MB到幾GB。正是因爲Heap空間由程序員管理,所以容易出現使用不當導致嚴重問題。
  • Android中的進程

    1. native進程:採用C/C++實現,不包含dalvik實例的linux進程,/system/bin/目錄下面的程序文件運行後都是以native進程形式存在的。如下圖/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native進程。

    2. java進程:實例化了dalvik虛擬機實例的linux進程,進程的入口main函數爲java函數。dalvik虛擬機實例的宿主進程是fork()系統調用創建的linux進程,所以每一個android上的java進程實際上就是一個linux進程,只是進程中多了一個dalvik虛擬機實例。因此,java進程的內存分配比native進程複雜。如圖3,Android系統中的應用程序基本都是java進程,如桌面、電話、聯繫人、狀態欄等等。
       這裏寫圖片描述
       

  • Android中進程的堆內存

    第一張圖和下面這張圖分別介紹了native process和java process的結構,這個是我們需要深刻理解的,進程空間中的heap空間是我們需要重點關注的。heap空間完全由程序員控制,我們使用的malloc、C++ new和java new所申請的空間都是heap空間, C/C++申請的內存空間在native heap中,而java申請的內存空間則在dalvik heap中。

    這裏寫圖片描述

    注:Java中的code segment,data segment,heap,stack
    stack(棧):對象引用都是在棧裏的,相當於C/C++的指針
    heap(堆):new出來的對象實例纔是在堆裏
    data segment:一般存放常量和靜態常量
    code segment:方法,函數什麼的都是放在code segment
    
  • Bitmap分配在native heap還是dalvik heap上?

     3.0後是分配在dalvik heap上,和3.x之前是分配在native heap

4. 以上主要來自:現任支付寶大神餘龍飛著作——Android進程的內存管理分析

二、內存分析之MAT

1. 谷歌提供了幾種內存檢測工具:

  • Memory Monitor:內存監視器
    這裏寫圖片描述
  • Heap Viewer:堆查看器
    這裏寫圖片描述
    這裏寫圖片描述
  • Allocation Tracker:分配追蹤器
    這裏寫圖片描述
  • Investigating Your RAM Usage:調查您的RAM使用

    通過Log輸出的GC命令來判斷:
    
    GC_CONCURRENT:heap快滿了
    GC_FOR_MALLOC:因爲你的應用程序試圖分配內存時,你已經充分引起GC堆,所以系統必須停止你的應用和回收內存。
    GC_HPROF_DUMP_HEAP:GC發生時,你要創建的請求HPROF文件來分析你的堆。
    GC_EXPLICIT:一個明確的GC,當收到調用gc()時出現,應該儘量避免手動調用,而是相信GC會自動清理
    GC_EXTERNAL_ALLOC:只會在API10以及以下才會出現
    
    
    GC的原因:
    Concurrent:不會暫停應用線程,在後臺運行,不會影響內存分配
    Alloc:GC是因爲你的應用程序試圖分配內存時,你heapwas已滿。在這種情況下,垃圾收集發生在分配線程。
    Explicit:手動調用gc(),我們應該避免手動調用,我們要相信GC,手動調用會影響線程分配以及沒必要的cpu週期,還可能導致其他線程的搶佔。
    NativeAlloc:native內存的回收,主要來自人爲造成的native內存壓力,例如:Bitmap、渲染腳本分配的對象
    CollectorTransition:.....由於用到的太少,後面的就不再詳述
    

2. 觸發內存泄漏

  • 多次切換屏幕的橫縱,在Activity不同狀態旋轉屏幕,然後再返回。旋轉設備往往會導致應用程序的Activity、Context、或View對象泄漏,因爲系統中重新創建活動,如果程序中其他地方擁有這些對象的引用,系統無法回收。
  • 多個應用之間切換,在Activity不同的狀態下切換(導航至主屏幕,然後返回到您的應用程序)。

3. 怎樣的內存是健康的?

  • 內存使用率低,使用率穩定(波動小)
  • 沒有正在使用的對象,要能夠被GC回收(避免內存泄漏)
  • 不再使用的內存對象、或着大型內存,使用結束(虛引用)馬上回收(finalize()方法進行清理,通過 java.lang.ref.PhantomReference實現)

    我們進行內存分析具體分析什麼呢?
    1. 大型對象
    2. 不使用的未能被釋放的對象(內存泄漏)
    
    而谷歌目前提供的內存分析工具只能從宏觀上進行內存分析,無法針對某個對象進行分析
    
    這裏我們這裏需要使用強大的第三方內存分析工具MAT(Memory Analyzer Tool)針對具體內存進行分析
    

4. MAT基礎知識

  • MAT簡介

    • MAT(Memory Analyzer Tool),一個基於Eclipse的內存分析工具,是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。使用內存分析工具從衆多的對象中進行分析,快速的計算出在內存中對象的佔用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的查看到可能造成這種結果的對象。
    • 當然MAT也有獨立的不依賴Eclipse的版本,只不過這個版本在調試Android內存的時候,需要將DDMS生成的文件進行轉換,纔可以在獨立版本的MAT上打開。不過Android SDK中已經提供了這個Tools,所以使用起來也是很方便的。
  • MAT下載地址

  • MAT中重要概念介紹
    要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root 這幾個概念一定要弄懂。

    • Shallow heap
      Shallow size就是對象本身佔用內存的大小,不包含其引用的對象。

      1. 常規對象(非數組)的Shallow size有其成員變量的數量和類型決定。

      2. 數組的shallow size有數組元素的類型(對象類型、基本類型)和數組長度決定

        因爲不像c++的對象本身可以存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的byte[],char[], int[],所以我們如果只看對象本身的內存,那麼數量都很小。所以我們看到Histogram圖是以Shallow size進行排序的,排在第一位第二位的是byte,char 。

    • Retained Heap

      Retained Heap的概念:如果一個對象被釋放掉,那麼該對象引用的所有對象(包括被遞歸釋放的)佔用的heap也會被釋放。

      如果一個對象的某個成員new了一大塊int數組,那這個int數組也可以計算到這個對象中。與shallow heap比較,Retained heap可以更精確的反映一個對象實際佔用的大小(因爲如果該對象釋放,retained heap都可以被釋放)。

      注意:A和B都引用到同一內存,A釋放時,該內存不會被釋放。所以這塊內存不會被計算到A或者B的Retained Heap中。故Retained Heap並不總是那麼有效。

      這一點並不重要,因爲MAT引入了Dominator Tree--對象引用樹,計算Retained Heap就會非常方便,顯示也非常方便。對應到MAT UI上,在dominator tree這個view中,顯示了每個對象的shallow heap和retained heap。然後可以以該節點爲樹根,一步步的細化看看retained heap到底是用在什麼地方了。

    • GC Root

      GC發現通過任何reference chain(引用鏈)無法訪問某個對象的時候,該對象即被回收。

      這裏寫圖片描述

      這裏寫圖片描述

      名詞GC Roots正是分析這一過程的起點,例如JVM自己確保了對象的可到達性(那麼JVM就是GC Roots),所以GC Roots就是這樣在內存中保持對象可到達性的,一旦不可到達,即被回收。

      通常GC Roots是一個在current thread(當前線程)的call stack(調用棧)上的對象(例如方法參數和局部變量),或者是線程自身或者是system class loader(系統類加載器)加載的類以及native code(本地代碼)保留的活動對象。所以GC Roots是分析對象爲何還存活於內存中的利器。

  • MAT界面功能介紹

    • 打開經過轉換的hprof文件:
      這裏寫圖片描述
      可不選
    • Actions區域,幾種分析方法:

      • Histogram:列出內存中的對象,對象的個數以及大小
        這裏寫圖片描述

      • Dominator Tree:列出最大的對象以及其依賴存活的Object (大小是以Retained Heap爲標準排序的)
        這裏寫圖片描述

      點開每個對象,檢查內部的超大對象

      • Top Consumers : 通過圖形列出最大的object
        這裏寫圖片描述

      一般Histogram和 Dominator Tree是最常用的。

  • MAT分析對象的引用

    • Path to GC Root
      在Histogram或者Domiantor Tree的某一個條目上,右鍵可以查看其GC Root Path:
      這裏寫圖片描述

    • 點擊Path To GC Roots –> with all references

      這裏寫圖片描述

      通過這個圖查看(內存泄漏)該內存還被誰所引用,爲何還不能釋放

  • MAT基礎介紹來自Gracker

三、內存問題總覽

  • 內存泄漏

    • 非靜態內部類的靜態實例容易造成內存泄漏
      非靜態內部類的存活需要依賴外部類

    • Activity使用靜態成員(靜態成員引用Drawable、Bitmap等大內存對象)

    • 使用handler時的內存問題
      因爲Handler的非即時性,導致部分代碼不能及時釋放
      可以使用Badoo開發的第三方的 WeakHandler

    • 註冊某個對象後未反註冊
      註冊廣播接收器、註冊觀察者等等

    • 集合中對象沒清理造成的內存泄露

    • 資源對象沒關閉造成的內存泄露
      資源性對象比如(Cursor,File文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。

  • 一些不良代碼成內存壓力

    • 循環以及遞歸
    • 數據隨意申請大小
    • 如果沒有用到不要定義全局變量
  • Bitmap使用不當

    • 及時的銷燬

    雖然,系統能夠確認Bitmap分配的內存最終會被銷燬,但是由於它佔用的內存過多,所以很可能會超過Java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機一個暗示:“該圖片可以釋放了”。

    • 設置一定的採樣率(二次採樣)
      有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設置一定的採樣率,那麼就可以大大減小佔用的內存。

    • 巧妙的運用軟引用(SoftRefrence)

      有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法調用Recycle函數。這時候巧妙的運用軟引用,可以使Bitmap在內存快不足時得到有效的釋放
      目前但凡是個圖片加載框架都會使用SoftRefrence

  • 構造Adapter時,沒有使用緩存的 convertView

  • 頻繁的方法中創建對象
    不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用 hashtable , vector 創建一組對象容器,然後從容器中去取那些對象,而不用每次 new 之後又丟棄。

四、 MAT內存分析

  • 使用Dominator Tree -> Path To GC Roots –> with all references
    上面MAT基礎裏面已經講

  • 根據某種類型的對象個數來分析內存泄漏。
    Actions -> Histogram
    這裏寫圖片描述

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

    這裏寫圖片描述

    這裏寫圖片描述

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

    這裏寫圖片描述

  1. 講的是對象及其應用的內存大小
  2. 講的是大型對象被誰所引用

內存分析工具還有一個比較流行的內存泄漏檢測庫:LeakCannary

發佈了39 篇原創文章 · 獲贊 9 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章