Android效能優化(三)之記憶體管理

頭條祁同偉發表於2017-02-28

1、初識記憶體優化

在Android的效能優化的各個部分裡,記憶體的問題絕對是最令人頭疼的一部分,雖然Android有垃圾自動回收機制不需要手動干預,但也恰因為此,出現記憶體問題如記憶體洩漏和記憶體溢位等,如果對記憶體管理機制不熟悉,會更加難以排查問題。

因為記憶體方面的知識較多且不易理解,記憶體優化部分就分兩篇文章進行,本文主要是關於Java、Android的記憶體分配、回收、GC等理論知識。

2、記憶體分配

談Android的記憶體,就不能不提Java的記憶體管理。Java程式在執行的過程中會將其管理的記憶體分為若干個不同的資料區:

Android效能優化(三)之記憶體管理
JVM執行時資料區

方法區:方法區存放的是類資訊、常量、靜態變數,所有執行緒共享區域。

虛擬機器棧:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊,執行緒私有區域。

本地方法棧:與虛擬機器棧類似,區別是虛擬機器棧為虛擬機器執行Java方法服務,本地方法棧為虛擬機器使用到的Native方法服務

JVM管理的記憶體中最大的一塊,所有執行緒共享;用來存放物件例項,幾乎所有的物件例項都在堆上分配記憶體;此區域也是垃圾回收器(Garbage Collection)主要的作用區域,記憶體洩漏就發生在這個區域

程式計數器可看做是當前執行緒所執行的位元組碼的行號指示器;如果執行緒在執行Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果執行的是Native方法,這個計數器的值為空(Undefined)。

備註:
有一種習慣說法:把Java的記憶體區域分為堆記憶體(Heap)和棧記憶體(Stack),Stack訪問快,Heap訪問慢,Stack中儲存的是物件的引用(指標),Heap中儲存的是物件的例項。

實際上這種說法是籠統、粗糙的,此處所說的Stack僅僅是虛擬機器棧中的區域性變數表部分。虛擬機器棧與JVM執行時資料區涵蓋的都比此種說法多。

3、記憶體回收

3.1標記-清除演算法

最基礎的收集演算法:分為“標記”和“清除”兩個階段,首先,標記出所有需要回收的物件,然後統一回收所有被標記的物件。
這種方法有兩個不足點:

  1. 效率問題,標記和清除兩個過程的效率都不高;
  2. 空間問題,標記清除之後會產生大量的不連續的記憶體碎片。

Android效能優化(三)之記憶體管理
“標記-清除”演算法示意圖

3.2複製演算法

將記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體將用完了,就將還存活著的物件複製到另一塊記憶體上面,然後再把已使用過的記憶體空間一次清理掉。
這種方法的特點:

  • 優點:實現簡單,執行高效;每次都是對整個半區進行記憶體回收,記憶體分配時也不需要考慮記憶體碎片等情況,只要移動堆頂指標,按順序分配記憶體即可;
  • 缺點:粗暴的將記憶體縮小為原來的一半,代價實在有點高。

Android效能優化(三)之記憶體管理
“複製”演算法示意圖

3.3標記-整理演算法

先標記需要回收的物件(標記過程與“標記-清除”演算法一樣),然後把所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
這種方法的特點:

  • 避免了記憶體碎片;
  • 避免了“複製”演算法50%的空間浪費;
  • 主要針對物件存活率高的老年代。

Android效能優化(三)之記憶體管理
“標記-整理”演算法示例圖.png

3.4分代收集演算法

根據物件的存活週期的不同將記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都會發現有大量物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除演算法或標記—整理演算法來進行回收。

4、物件是否回收的依據

4.1引用計數演算法

給物件中新增一個引用計數器,每當有一個地方引用該物件時,計數器值加1;引用失效時,計數器值減1;任意時刻計數器為0的物件就是不可能再被使用的,表示該物件不存在引用關係。
這種方法的特點:

  • 優點:實現簡單,判定效率也很高;
  • 缺點:難以解決物件之間相互迴圈引用導致計數器值不等於0的問題。

4.2可達性分析演算法

以一系列成為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(GC Roots到這個物件不可達),則證明此物件是不可用的。

Android效能優化(三)之記憶體管理
可達性分析演算法判定物件是否可回收

5、Android的記憶體管理

Android系統的ART和Dalvik虛擬機器扮演了常規的記憶體垃圾自動回收的角色, 使用pagingmemory-mapping來管理記憶體,這意味著不管是因為建立物件還是使用使用記憶體頁面造成的任何被修改的記憶體,都會一直存在於記憶體中,App唯一釋放記憶體的方法就是釋放App持有的物件引用,使GC可以回收。

Android效能優化(三)之記憶體管理
Android Runtime記憶體堆劃分

  • 5.1記憶體回收

    在Android的高階系統版本里面針對Heap空間有一個Generational Heap Memory的模型,最近分配的物件會存放在Young Generation區域,當這個物件在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後累積一定時間再移動到Permanent Generation區域。系統會根據記憶體中不同的記憶體資料型別分別執行不同的gc操作。例如,剛分配到Young Generation區域的物件通常更容易被銷燬回收,同時在Young Generation區域的gc操作速度會比Old Generation區域的gc操作速度更快。

  • 5.2共享記憶體

    1. Android應用的程式都是從一個叫做Zygote的程式fork出來的。Zygote程式在系統啟動並且載入通用的framework的程式碼與資源之後開始啟動。為了啟動一個新的程式程式,系統會fork Zygote程式生成一個新的程式,然後在新的程式中載入並執行應用程式的程式碼。這使得大多數的RAM pages被用來分配給framework的程式碼,同時使得RAM資源能夠在應用的所有程式之間進行共享。
    2. 大多數static的資料被mmapped到一個程式中。這不僅僅使得同樣的資料能夠在程式間進行共享,而且使得它能夠在需要的時候被paged out。常見的static資料包括Dalvik Code,app resources,so檔案等。
    3. 大多數情況下,Android通過顯式的分配共享記憶體區域(例如ashmem或者gralloc)來實現動態RAM區域能夠在不同程式之間進行共享的機制。例如,Window Surface在App與Screen Compositor之間使用共享的記憶體,Cursor Buffers在Content Provider與Clients之間共享記憶體。
  • 5.3分配與回收記憶體

    1. 每一個程式的Dalvik heap都反映了使用記憶體的佔用範圍。這就是通常邏輯意義上提到的Dalvik Heap Size,它可以隨著需要進行增長,但是增長行為會有一個系統為它設定的上限。
    2. 邏輯上講的Heap Size和實際物理意義上使用的記憶體大小是不對等的,Proportional Set Size(PSS)記錄了應用程式自身佔用以及和其他程式進行共享的記憶體。
  • 5.4限制應用的記憶體

    1. 為了整個Android系統的記憶體控制需要,Android系統為每一個應用程式都設定了一個硬性的Dalvik Heap Size最大限制閾值,這個閾值在不同的裝置上會因為RAM大小不同而各有差異。如果你的應用佔用記憶體空間已經接近這個閾值,此時再嘗試分配記憶體的話,很容易引起OutOfMemoryError的錯誤。
    2. ActivityManager.getMemoryClass()可以用來查詢當前應用的Heap Size閾值,這個方法會返回一個整數,表明你的應用的Heap Size閾值是多少Mb(megabates)。
  • 5.5應用切換

    1. Android系統並不會在使用者切換應用的時候做交換記憶體的操作。Android會把那些不包含Foreground元件的應用程式放到LRU Cache中。例如,當使用者開始啟動了一個應用,系統會為它建立了一個程式,但是當使用者離開這個應用,此程式並不會立即被銷燬,而是會被放到系統的Cache當中,如果使用者後來再切換回到這個應用,此程式就能夠被馬上完整的恢復,從而實現應用的快速切換。
    2. 如果你的應用中有一個被快取的程式,這個程式會佔用一定的記憶體空間,它會對系統的整體效能有影響。因此當系統開始進入Low Memory的狀態時,它會由系統根據LRU的規則與應用的優先順序,記憶體佔用情況以及其他因素的影響綜合評估之後決定是否被殺掉。

需要特別注意的:

  • 在Dalvik下,大部分Davik採取的都是標記-清理回收演算法,而且具體使用什麼演算法是在編譯期決定的,無法在執行的時候動態更換。標記-清理回收演算法無法對Heap中空閒記憶體區域做碎片整理。系統僅僅會在新的記憶體分配之前判斷Heap的尾端剩餘空間是否足夠,如果空間不夠會觸發gc操作,從而騰出更多空閒的記憶體空間;這樣記憶體空洞就產生了。

Android效能優化(三)之記憶體管理
記憶體碎片的產生

如上圖所示,第一行,在開始階段,記憶體分配較滿;第二行,經過GC之後,大部分物件被釋放。此時可能產生的問題是,因為沒有記憶體整理功能,整個頁面的4KB記憶體(記憶體分配的最小單位是頁面,通常為4KB)可能只有一個小物件,但是統計PrivateDirty/Pss時還是按照4KB計算。所以對於Dalvik虛擬機器的手機來說,我們首先要儘量避免掉頻繁生成很多臨時小變數(比如說:getView, onDraw等函式中new物件),另一個又要儘量去避免產生很多長生命週期的大物件。

  • ART在GC上不像Dalvik僅有一種回收演算法,ART在不同的情況下會選擇不同的回收演算法。應用程式在前臺執行時,響應性是最重要的,因此也要求執行的GC是高效的。相反,應用程式在後臺執行時,響應性不是最重要的,這時候就適合用來解決堆的記憶體碎片問題。因此,Mark-Sweep GC適合作為Foreground GC,而Mark-Compact GC適合作為Background GC。由於有Compact的能力存在,記憶體碎片在ART上可以很好的被避免,這個也是ART一個很好的能力。

六、Android GC何時發生?

由上文我們知道,GC操作主要是由系統決定的,但是我們可以監聽系統的GC過程,以此來分析我們應用程式當前的記憶體狀態。
Dalvik虛擬機器,每一次GC列印內容格式:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>複製程式碼

含義解析

  • GC Reason:GC觸發原因
    GC_CONCURRENT:當已分配記憶體達到某一值時,觸發併發GC;
    GC_FOR_MALLOC:當嘗試在堆上分配記憶體不足時觸發的GC;系統必須停止應用程式並回收記憶體;
    GC_HPROF_DUMP_HEAP: 當需要建立HPROF檔案來分析堆記憶體時觸發的GC;
    GC_EXPLICIT:當明確的呼叫GC時,例如呼叫System.gc()或者通過DDMS工具顯式地告訴系統進行GC操作等;
    GC_EXTERNAL_ALLOC: 僅在API級別為10或者更低時(新版本分配記憶體都在Dalvik堆上)
  • Amount freed GC:回收的記憶體大小
  • Heap stats:堆上的空閒記憶體百分比 (已用記憶體)/(堆上總記憶體)
  • External memory stats: API級別為10或者更低:(已分配的記憶體量)/ (即將發生垃圾的極限)
  • Pause time:這次GC操作導致應用程式暫停的時間。關於這個暫停的時間,在2.3之前GC操作是不能併發進行的,也就是系統正在進行GC,那麼應用程式就只能阻塞住等待GC結束。而自2.3之後,GC操作改成了併發的方式進行,就是說GC的過程中不會影響到應用程式的正常執行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間。

Art虛擬機器,每一次GC列印內容格式:

I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>複製程式碼

基本情況和Dalvik沒有什麼差別,GC的Reason更多了,還多了一個LOS_Space_Status.

LOS_Space_Status:Large Object Space,大物件佔用的空間,這部分記憶體並不是分配在堆上的,但仍屬於應用程式記憶體空間,主要用來管理 Bitmap 等佔記憶體大的物件,避免因分配大記憶體導致堆頻繁 GC。

七、獲取記憶體使用情況

通過命令列adb shell dumpsys meminfo packagename檢視記憶體詳細佔用情況:

Android效能優化(三)之記憶體管理
命令列檢視記憶體分配情況
其中幾個關鍵的資料:

  • Private(Clean和Dirty的):應用程式單獨使用的記憶體,代表著系統殺死你的程式後可以實際回收的記憶體總量**。通常需要特別關注其中更為昂貴的dirty部分,它不僅只被你的程式使用而且會持續佔用記憶體而不能被從記憶體中置換出儲存。申請的全部Dalvik和本地heap記憶體都是Dirty的,和Zygote共享的Dalvik和本地heap記憶體也都是Dirty的。
  • Dalvik Heap:Dalvik虛擬機器使用的記憶體,包含dalvik-heap和dalvik-zygote,堆記憶體,所有的Java物件例項都放在這裡。
  • Heap Alloc:累加了Dalvik和Native的heap。
  • PSS:這是加入與其他程式共享的分頁記憶體後你的應用佔用的記憶體量,你的程式單獨使用的全部記憶體也會加入這個值裡,多程式共享的記憶體按照共享比例新增到PSS值中。如一個記憶體分頁被兩個程式共享,每個程式的PSS值會包括此記憶體分頁大小的一半在內。
    Dalvik Pss記憶體 = 私有記憶體Private Dirty + (共享記憶體Shared Dirty / 共享程式數)
  • TOTAL:上面全部條目的累加值,全域性的展示了你的程式佔用的記憶體情況。
  • ViewRootImpl:應用程式裡的活動視窗檢視個數,可以用來監測對話方塊或者其他視窗的記憶體洩露。
  • AppContexts及Activities:應用程式裡Context和Activity的物件個數,可以用來監測Activity的記憶體洩露。

參考:

歡迎關注微信公眾號:定期分享Java、Android乾貨!

Android效能優化(三)之記憶體管理
歡迎關注

相關文章