讀書筆記,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)
前言
本文分為兩部分,第一部分為 《Garbage Collection in Android》 的翻譯,第二部分簡介 Android 虛擬機器與 Java 虛擬機器的差別。
Garbage Collection in Android
演講人介紹
Colt McAnlis,Google 開發工程師。為便於寫作,筆者將以第一人稱視角對視訊內容進行概述。

自動記憶體管理的陷進
很多高效能語言,如 C 和 C++ 都需要開發人員手動管理記憶體的分配與釋放,但在程式碼量很大、業務邏輯很複雜時,很容易忘記釋放已分配的記憶體,進而導致記憶體洩露。位於 Android Runtime 的 Dalvik(< Android 5.0) 或 ART(≥ Android 5.0)虛擬機器的自動垃圾回收機制,將開放人員從手動管理記憶體的工作方式中解放了出來,從而提高了工作效率。但同時也隱藏了效能陷進,其中最需要關注的就是如何分配以及使用記憶體。
GC 概念由來
自動記憶體管理機制稱為 Garbage Collection,是 John McCarthy 於1959 年提出的概念,用於解決 LISP 語言中的問題。它主要涉及兩個原則:
- 找出不再訪問的物件;
- 回收這些物件佔用的資源。

GC 實現的難點
想象一下,你分配了 20,000 個物件,那麼怎麼識別哪些物件是不再訪問的,或者什麼時候應該觸發 GC 事件呢?

這真是太難了!好在,自 GC 概念提出之後的 50 年裡,我們一直致力於提升垃圾收集器的效能,這也是為什麼 Android 的垃圾回收器比 John McCarthy 提出的複雜的多的原因。
Android GC 簡介
事實上,Android 系統根據所分配物件的型別以及 GC 時系統如何管理這些物件,將程式所使用的記憶體分成了多個空間。新分配的物件位於什麼空間,取決於你的 Android Runtime 是什麼版本,5.0 以上是 ART(Android Runtime)虛擬機器,5.0以下是 Dalvik 虛擬機器。

筆者注:上圖具體含義見下文差異分析。
每個空間都有大小限制(Set Size),系統會跟蹤整個程式所佔用的記憶體大小。當程式佔用記憶體達到一定程度時,系統就會觸發 GC 事件回收記憶體,以便將來分配給其它物件:

GC 事件在 Dalvik 虛擬機器和 ART 虛擬機器上的表現也不盡相同。在 Dalvik 虛擬機器中,很多 GC 事件都是 "Stop the World Event":

而 ART 虛擬機器擴充套件了並行 GC 演算法,消除了大的 GC 停頓時間,但是在重要步驟上,還是會有短暫的停頓:

GC 導致的掉幀
儘管系統工程師做了大量優化來提升 GC 速度來減少卡頓,但是你的 App 仍然可能存在效能問題。我們知道,Android 系統每 16 ms 就要渲染一幀,所以在一幀時間內,GC 時間越長,留給業務邏輯的時間就越短:

如果你的 GC 過於頻繁(比如在迴圈中建立了大量臨時物件)或 GC 時間過長,就會導致 1 幀的處理時間超過 16 ms 上限,這就會給使用者造成卡頓、掉幀的視覺效果:

記憶體分析工具
好在,Android Studio 的 Profiler 工具,可以用來檢視記憶體使用情況以及記憶體分配情況。
總結
不過,記憶體優化就是說起來簡單,做起來難,所以你還需要觀看以下視訊來掌握更多的效能優化知識:

Android 虛擬機器與 Java 虛擬機器的差異
其實筆者主要想關注的這幾個虛擬機器在記憶體佈局及 GC 演算法方面的差異,至於 JVM、Dalvik、ART 各自對應的可執行檔案格式(.jar、.dex、.elf)、位元組碼結構(class、dex、elf)這顯然是不同的。奈何關於 Android 虛擬機器記憶體佈局網上資料甚少,大部分只圍繞執行時堆記憶體的分配及其 GC 演算法來講,沒有涉及虛擬機器棧(方法執行模型)、常量池、方法區,所以這部分沒法跟 Java 虛擬機器進行對比。不過,萬物之間是存在普遍聯絡的,沒有東西可以憑空產生。既然 Java 虛擬機器的方法執行模型都能跟 C 語言在概念上很相似,Dalvik 和 ART 自然也可以照此理解,細節上肯定是不一樣的,畢竟指令集都不一樣。真想深究,只能看 虛擬機器原始碼 了。
不鑽牛角尖了,頭疼。再來看下這個圖:

這圖描述的就是 Dalvik 和 ART 虛擬機器對執行時堆的空間劃分,這個在原始碼中都有對應的實現。 上圖具體含義可參考:
或者使用 看雲 閱讀更方便。
總的來說,就是 Android 虛擬機器沒有使用 HotSpot 虛擬機器所採用的分代收集演算法,而是採用了標記-清除或者標記-複製演算法,這個可以在編譯系統時指定,可參考《Android Garbage Collection/dalvik GC》,不過一般都是標記清除演算法。
以下摘自 《Android虛擬機器之Dalvik虛擬機器》:
- 記憶體管理
◇ Java Object Heap 大小受限,如:16M/24M/32M/48M
◇ Bitmap Memory(External Memroy):大小計入 Java Object Heap
◇ Native Heap:大小不受限- 垃圾收集(GC)
◇ Mark:使用RootSet標記物件引用
◇ Sweep:回收沒有被引用的物件- GingerBread(Android 2.3)之前
◇ Stop-the-word:也就是垃圾收集執行緒在執行的時候,其它的執行緒都停止
◇ Full heap collection:也就是一次收集完全部的垃圾,一次垃圾收集造成的程式中止時間通常都大於 100ms- GingerBread(Android 2.3)之後
◇ Cocurrent:也就是大多數情況下,垃圾收集執行緒與其它執行緒是併發執行的
◇ Partial collection:也就是一次可能只收集一部分垃圾,一次垃圾收集造成的程式中止時間通常都小於 5ms
ART 虛擬機器對並行 GC 進行了擴充套件,將堆記憶體劃分成更多不同型別的具體空間,使用不同的 GC 演算法以獲得更短的 GC 停頓時間。
Dalvik 虛擬機器
wiki
Dalvik 虛擬機器,是 Google 等廠商合作開發的 Android 移動裝置平臺的核心組成部分之一。它可以支援已轉換為 .dex(即“Dalvik Executable”)格式的 Java 應用程式的執行。.dex 格式是專為 Dalvik 設計的一種壓縮格式,適合記憶體和處理器速度有限的系統。Dalvik 由 Dan Bornstein 編寫的,名字來源於他的祖先曾經居住過的小漁村達爾維克(Dalvík),位於冰島 Eyjafjörður。
大多數虛擬機器包括 JVM 都是一種堆疊機器,而 Dalvik 虛擬機器則是暫存器機。兩種架構各有優劣,一般而言,基於堆疊的機器需要更多指令,而基於暫存器的機器指令更長。
從 Android 5.0 版起,Android Runtime(ART)取代 Dalvik 成為系統內預設虛擬機器。
差異:
- Dalvik 虛擬機器早期並沒有使用即時編譯(JIT)技術。從 Android 2.2 開始, Dalvik 虛擬機器也支援 JIT.
- Dalvik 虛擬機器有自己的位元組碼,並非使用 Java 位元組碼。
- Dalvik 基於暫存器,而 JVM 基於堆疊。
- Dalvik VM 透過 Zygote 進行類別的預載入,Zygote 會完成虛擬機器的初始化,也是與 JVM 不同之處。
標記-清除演算法(Mark-Sweep Algorithm)
Dalvik 虛擬機器採用 Mark-Sweep 演算法,不帶壓縮整理(Compact),所以比 Java 虛擬機器更簡單一些。

(a)GC 前的狀態。示例中有一個 GC Root,所有物件都未被標記。
(b)GC 標記後的狀態。在標記階段,所有活動物件(Active Objects)都會被標記。
(c)GC 清除後的狀態。所有垃圾已被回收,並且所有活動物件的標記狀態都被重置為 false。
GC 觸發時機
- 即將產生 OOM 時
- 堆記憶體使用達到上限時(系統可配)
- 顯示呼叫 gc()
GC 日誌
在 Dalvik(而不是 ART)中,每次垃圾回收都會將以下資訊列印到 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_CONCURRENT:在您的堆開始佔用記憶體時可以釋放記憶體的併發垃圾回收。
- GC_FOR_MALLOC:堆已滿而系統不得不停止您的應用並回收記憶體時,您的應用嘗試分配記憶體而引起的垃圾回收。
- GC_HPROF_DUMP_HEAP:當您請求建立 HPROF 檔案來分析堆時出現的垃圾回收。
- GC_EXPLICIT:顯式垃圾回收,例如當您呼叫 gc() 時(您應避免呼叫,而應信任垃圾回收會根據需要執行)。
- GC_EXTERNAL_ALLOC:這僅適用於 API 級別 10 及更低階別(更新版本會在 Dalvik 堆中分配任何記憶體)。外部分配記憶體的垃圾回收(例如儲存在原生記憶體或 NIO 位元組緩衝區中的畫素資料)。
釋放量
從此次垃圾回收中回收的記憶體量。
堆統計資料
堆的可用空間百分比與(活動物件數量)/(堆總大小)。
外部記憶體統計資料
API 級別 10 及更低階別的外部分配記憶體(已分配記憶體量)/(發生回收的限值)。
暫停時間
堆越大,暫停時間越長。併發暫停時間顯示了兩個暫停:一個出現在回收開始時,另一個出現在回收快要完成時。 在這些日誌訊息積聚時,請注意堆統計資料的增大(上面示例中的 3571K/9991K 值)。如果此值繼續增大,可能會出現記憶體洩漏。
ART 虛擬機器
wiki
Android Runtime(縮寫為ART),是一種在 Android 作業系統上的執行環境,由 Google 公司研發,並在 2013 年作為 Android 4.4 系統中的一項測試功能正式對外發布,在 Android 5.0 及後續 Android 版本中作為正式的執行時庫取代了以往的 Dalvik 虛擬機器。ART 能夠把應用程式的位元組碼轉換為機器碼,是 Android 所使用的一種新的虛擬機器。它與 Dalvik 的主要不同在於:Dalvik 採用的是 JIT 技術,而 ART 採用 Ahead-of-time(AOT)技術。ART 同時也改善了效能、垃圾回收(Garbage Collection)、應用程式出錯以及效能分析。
JIT 最早在 Android 2.2 系統中引進到 Dalvik 虛擬機器中,在應用程式啟動時,JIT 通過進行連續的效能分析來優化程式程式碼的執行,在程式執行的過程中,Dalvik 虛擬機器在不斷的進行將位元組碼編譯成機器碼的工作。與 Dalvik 虛擬機器不同的是,ART 引入了 AOT 這種預編譯技術,在應用程式安裝的過程中,ART 就已經將所有的位元組碼重新編譯成了機器碼。應用程式執行過程中無需進行實時的編譯工作,只需要進行直接呼叫。因此,ART 極大的提高了應用程式的執行效率,同時也減少了手機的電量消耗,提高了移動裝置的續航能力,在垃圾回收等機制上也有了較大的提升。為了保證向下相容,ART 使用了相同的 Dalvik 位元組碼檔案(dex),即在應用程式目錄下保留了 dex 檔案供舊程式呼叫然而 .odex 檔案則替換成了可執行與可連結格式(ELF)可執行檔案。一旦一個程式被 ART 的 dex2oat 命令編譯,那麼這個程式將會指通過 ELF 可執行檔案來執行。因此,相對於 Dalvik 虛擬機器模式,ART 模式下 Android 應用程式的安裝需要消耗更多的時間,同時也會佔用更大的儲存空間(指內部儲存,用於儲存編譯後的程式碼),但節省了很多 Dalvik 虛擬機器用於實時編譯的時間。
Google 公司在 Android 4.4 中帶來的 ART 模式僅僅是 ART 的一個預覽版,系統預設仍然使用的是 Dalvik 虛擬機器,4.4 上面提供的預覽版 ART 相對於 Android 5.0 以後的 ART 執行時庫有較大的不同,尤其體現在相容性上。
GC 日誌
與 Dalvik 不同,ART 不會為未明確請求的垃圾回收記錄訊息。只有在認為垃圾回收速度較慢時才會列印垃圾回收。更確切地說,僅在垃圾回收暫停時間超過 5ms 或垃圾回收持續時間超過 100ms 時。如果應用未處於可察覺的暫停程式狀態,那麼其垃圾回收不會被視為較慢。始終會記錄顯式垃圾回收。
ART 會在其垃圾回收日誌訊息中包含以下資訊:
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
複製程式碼
垃圾回收原因
什麼觸發了垃圾回收以及是哪種回收。可能出現的原因包括:
- Concurrent:不會暫停應用執行緒的併發垃圾回收。此垃圾回收在後臺執行緒中執行,而且不會阻止分配。
- Alloc:您的應用在堆已滿時嘗試分配記憶體引起的垃圾回收。在這種情況下,分配執行緒中發生了垃圾回收。
- Explicit:由應用明確請求的垃圾回收,例如,通過呼叫 gc() 或 gc()。與 Dalvik 相同,在 ART 中,最佳做法是您應信任垃圾回收並避免請求顯式垃圾回收(如果可能)。不建議使用顯式垃圾回收,因為它們會阻止分配執行緒並不必要地浪費 CPU 週期。如果顯式垃圾回收導致其他執行緒被搶佔,那麼它們也可能會導致卡頓(應用中出現間斷、抖動或暫停)。
- NativeAlloc:原生分配(如點陣圖或 RenderScript 分配物件)導致出現原生記憶體壓力,進而引起的回收。
- CollectorTransition:由堆轉換引起的回收;此回收由執行時切換垃圾回收引起。回收器轉換包括將所有物件從空閒列表空間複製到碰撞指標空間(反之亦然)。當前,回收器轉換僅在以下情況下出現:在 RAM 較小的裝置上,應用將程式狀態從可察覺的暫停狀態變更為可察覺的非暫停狀態(反之亦然)。
- HomogeneousSpaceCompact[/ˌhɒmə(ʊ)'dʒiːnɪəs/]:齊性空間壓縮是空閒列表空間到空閒列表空間壓縮,通常在應用進入到可察覺的暫停程式狀態時發生。這樣做的主要原因是減少 RAM 使用量並對堆進行碎片整理。
- DisableMovingGc:這不是真正的垃圾回收原因,但請注意,發生併發堆壓縮時,由於使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情況下,強烈建議不要使用 GetPrimitiveArrayCritical,因為它在移動回收器方面具有限制。
- HeapTrim:這不是垃圾回收原因,但請注意,堆修剪完成之前回收會一直受到阻止。
垃圾回收名稱
ART 具有可以執行的多種不同的垃圾回收。
- Concurrent mark sweep (CMS):整個堆回收器,會釋放和回收映像空間(Image Space)以外的所有其他空間。
- Concurrent partial mark sweep:幾乎整個堆回收器,會回收除了映像空間(Image Space)和 zygote 空間以外的所有其他空間。
- Concurrent sticky mark sweep:生成回收器,只能釋放自上次垃圾回收以來分配的物件。此垃圾回收比完整或部分標記清除執行得更頻繁,因為它更快速且暫停時間更短。
- Marksweep + semispace:非併發、複製垃圾回收,用於堆轉換以及齊性空間壓縮(對堆進行碎片整理)。
釋放的物件
此次垃圾回收從非大型物件空間回收的物件數量。
釋放的大小
此次垃圾回收從非大型物件空間回收的位元組數量。
釋放的大型物件
此次垃圾回收從大型物件空間回收的物件數量。
釋放的大型物件大小
此次垃圾回收從大型物件空間回收的位元組數量。
堆統計資料
空閒百分比與(活動物件數量)/(堆總大小)。
暫停時間
通常情況下,暫停時間與垃圾回收執行時修改的物件引用數量成正比。當前,ART CMS 垃圾回收僅在垃圾回收即將完成時暫停一次。移動的垃圾回收暫停時間較長,會在大部分垃圾回收期間持續出現。
如果您在 logcat 中看到大量的垃圾回收,請注意堆統計資料的增大(上面示例中的 25MB/38MB 值)。如果此值繼續增大,且始終沒有變小的趨勢,則可能會出現記憶體洩漏。或者,如果您看到原因為“Alloc”的垃圾回收,那麼您的操作已經快要達到堆容量,並且將很快出現 OOM 異常。
附
這個網頁好像沒有英文版,但是有的中文解釋又很彆扭,比如“映像空間”,這是什麼鬼?其實就是 Image Space...