寫在前面
英文原版連結,若是覺得本文哪裡不好還請指出,以便及時修改,以下是譯文
安卓是為行動裝置設計的,所以開發者應該時刻留意 app 佔用的 RAM(Random-Access Memory)。儘管 Dalvik 和 ART 會例行垃圾回收(GC),但並不代表開發者可以忽略 app 的記憶體使用情況(什麼時候在哪裡分配/釋放了多少記憶體)。為了確保穩定的用戶體驗,讓系統能夠在多個 app 之間迅速切換,app 不要在用戶沒有和它交互時消耗不必要的記憶體,這一點很重要
P.S. Dalvik(Android 專用的 JVM)、ART(Android Runtime),Android4.4 引入了 ART,之前是 Dalvik
即便在開發期間遵循所有的最佳實踐來管理 app 的記憶體(開發者理應這樣做),也可能會出現物件洩漏或者其它記憶體 bug。唯一可靠的,能儘量減少 app 記憶體佔用的方法就是用工具分析 app 的記憶體使用情況,本篇指南將詳細說明應該怎麼做
Log 資訊解釋
查看 app 記憶體使用情況最簡單的方式是執行時的 log 資訊,GC 執行後,會輸出一段資訊到 logcat。logcat 的輸出可以在 Device Monitor 裡查看,或者直接在 IDE 比如 Android Studio 裡查看
Dalvik Log 資訊
在 Dalvik(不是 ART)模式下,每次 FC 會在 logcat 輸出如下資訊:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
例如:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
GC Reason
什麼觸發了 GC,以及屬於哪種類型的垃圾回收,可能出現的值包括:
- GC_CONCURRENT
heap 快滿了引起的並發 GC
- GC_FOR_MALLOC
heap 已經滿了,此時 app 嘗試分配記憶體,所以系統只好阻止 app 並回收記憶體,導致記憶體分配失敗
- GC_HPROF_DUMP_HEAP
請求建立一個用來分析 heap 的 HPROF 檔案,引發 GC
P.S. hprof 檔案描述了 heap 使用情況,類似於 Chrome 的 heapsnapshot 檔案
- GC_EXPLICIT
顯式 GC,例如呼叫 gc()(這種方式應該避免,要相信 GC 會在需要的時候自動執行)
- GC_EXTERNAL_ALLOC
外部記憶體分配(比如像素數據儲存在 native memory 裡或者 NIO byte buffer)引起的 GC,只會在 API 10 和更低版本中出現(新一點的版本所有東西都在 Dalvik heap 裡分配)
Amount freed
本次 GC 回收的記憶體大小
Heap stats
heap 中可用記憶體所佔百分比,以及 [已使用記憶體大小]/[heap 總大小]
External memory stats
外部記憶體統計數據,
在 API 10 和更低版本中,外部分配的記憶體,以及 [已分配記憶體大小]/[GC 下限]
P.S. 達到 GC 下限時進行 GC
Pause time
heap 越大,需要的停頓次數越多,並發 GC(GC Reason 為 GC_CONCURRENT)的停頓次數分 2 部分:第一部分是 GC 開始時,另一部分是 GC 結束時
記下這些 log 資訊,關注 heap stats(例子中是 3571K/9991K),如果持續增長,可能就存在記憶體洩漏了
ART Log 資訊
與 Dalvik 不同,ART 不會 log 非顯式(隱式)請求的 GC,GC 只會在被判定為很慢時輸出資訊。更準確地,條件是 GC 停頓超過 5ms,或者 GC 耗時超過 100ms。如果 app 不是處於一種可察覺的停頓狀態,那麼 GC 不會就被判定為很慢,而顯式 GC 會被 log 出來
ART 的 GC log 中包含了以下資訊:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
例如:
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
GC Reason
什麼觸發了 GC,以及屬於哪種類型的垃圾回收,可能出現的值包括:
- Concurrent
並發 GC,不會掛起 app 執行緒,這種 GC 在後台執行緒中運行,不會阻止記憶體分配
- Alloc
GC 被初始化,app 在 heap 已滿的時候請求分配記憶體,此時,GC 會在當前執行緒(請求分配記憶體的執行緒)執行
- Explicit
GC 被 app 顯式請求,例如,透過呼叫 System.gc() 或者 runtime.gc()。和 Dalvik 一樣,ART 建議相信 GC,儘可能地避免請求顯式 GC。不建議使用顯式 GC,因為會阻塞當前執行緒,並引起不必要的 CPU 週期。如果 GC 導致其它執行緒被搶佔的話,顯式 GC 還會引發 jank
P.S. jank 是指第 n 幀繪製過後,本該繪製第 n+1 幀,但因為 CPU 被搶佔,數據沒有準備好,只好再顯示一次第 n 幀,下一次繪製時顯示第 n+1 幀
- NativeAlloc
來自 native 分配的 native memory 壓力引起的 GC,比如 Bitmap 或者 RenderScript 物件
- CollectorTransition
heap 變遷引起的 GC,執行時動態切換 GC 造成的,垃圾回收器變遷過程包括從 free-list backed space 複製所有物件到 bump pointer space(反之亦然)。當前垃圾回收器過渡只會在低 RAM 設備的 app 改變運行狀態時發生,比如從可察覺的停頓態到非可察覺的停頓態(反之亦然)
- HomogeneousSpaceCompact
Homogeneous space compaction 是 free-list space 與 free-list space 的合併,經常在 app 變成不可察覺的停頓態時發生,這樣做的主要原因是減少 RAM 佔用並整理 heap 碎片
- DisableMovingGc
不是一個真正的 GC 原因,正在整理碎片的 GC 被 GetPrimitiveArrayCritical 阻塞,一般來說,因為 GetPrimitiveArrayCritical 會限制垃圾回收器過渡,強烈建議不要使用
- HeapTrim
不是一個真正的 GC 原因,但 GC 被阻塞,直到 heap trim 結束
GC Name
ART 有幾種不同的 GC
- Concurrent mark sweep (CMS)
全堆垃圾收集器,負責收集釋放除 image 外的所有空間
- Concurrent partial mark sweep
差不多是全堆垃圾收集器,負責收集除 image 和 zygote 外的所有空間
- Concurrent sticky mark sweep
分代垃圾收集器,只負責釋放從上次 GC 到現在分配的物件,該 GC 比全堆和部分標記清除(mark sweep)執行得更頻繁,因為它更快而且停頓更短
- Marksweep + semispace
非並發的,複製堆過渡和 homogeneous space compaction(用來整理 heap 碎片)使用的 GC
Objects freed
本次 GC 從非大物件空間(non large object space)回收的物件數目
Size freed
本次 GC 從非大物件空間回收的字節數
Large objects freed
本次 GC 從大物件空間裡回收的物件數目
Large object size freed
本次 GC 從大物件空間裡回收的字節數
Heap stats
可用空間所佔的百分比和 [已使用記憶體大小]/[heap 總大小]
Pause times
一般情況下,GC 執行時,停頓次數和被修改的物件引用數成比例。目前,ART CMS GC 只會在 GC 結束的時停頓一次,GC 過渡會有一個長停頓,是 GC 時耗的主要因素
如果在 logcat 看到一堆 GC 資訊,找到 heap stats(例子中是 25MB/38MB),如果持續增長從不減小,就可能存在記憶體洩漏。如果看到 GC 的原因是 Alloc,那麼說明 heap 已經要滿了,快 OOM 了
暫無評論,快來發表你的看法吧