本文將按照時間順序講述Android垃圾回收器(Garbage Collector)的演進,從最早版本的Android到最新版本。
縱觀整個演變的歷程,關於GC的變動,主要可以分為兩大類:
1. GC工作模式的改動。即Android的開發團隊對GC的整體邏輯進行了大的修改,改動包括 分配/釋放 內存的算法,以及搜索可達對象和確定可用內存的邏輯等。
2. 優化。即整體邏輯不變的基礎上對算法進行優化。
對改動的分類是有意義的,因為在某些版本,GC有明顯的改動。我們需要明確這些改動是重新實現還是優化,這樣可以更好的梳理出一些Android GC發展歷程中的關鍵節點。
整體來看,我們可以把Android的GC演變總結為四個階段:
1. Dalvik GC : 第一階段的GC,對應系統版本為Android KitKat之前。Dalvik GC使用了比較舊的"stop the world"設計,在垃圾回收期間會暫停虛擬機上的所有線程。
2. ART GC (Lollipop & Marshamllow) : GC發展歷程中最大的一次改動,ART/Dalvik的開發團隊重寫了整個GC。這一階段的GC也被叫做“分代GC”,因為對象會因存活時間不同而具有不同的“代”(或者叫年齡),另外還有其他方面的大的改進,比如分配內存的方式等。
3. ART GC(Nougat) : ART/Dalvik團隊用匯編重寫了整個內存分配的過程。
4. ART GC(Oreo) : 對第一版ART GC進行了優化,最明顯的改進是把垃圾回收過程改為並發執行。這一代的GC也叫“並發復制垃圾回收器(Concurrent Copying Garbage Collector)”,同時在其他方面也做了不少優化。
Dalvik
Android在KitKat之前一直使用Dalvik運行環境,在KitKat的時候谷歌引入了新開發的ART,和Dalvik混合發布,目的是進行一些前期的測試以及從開發人員那里獲取一些反饋。在Lollipop發布時就用ART全面替換掉了Dalvik。
Dalvik時期,對象的內存分配和回收都比較慢。因為Dalvik使用的GC是單線程的,在垃圾回收期間要暫停虛擬機上的其他所有線程。所以這個時期的開發建議就是盡可能不要分配內存(GC=卡頓,所以不分配,就不用回收了。。)
Dalvik GC采用了並發標記-清除(CMS)算法。標記-清除算法的特點是,不用的對象不會馬上回收,而是等到可用內存用盡之后才回收。這樣導致了很多對象在用完之后還會在內存中存活較長時間,不能及時釋放。
而且Dalvik GC只對后台的App進行內存整理(壓縮),導致(前台App)堆中出現大量內存碎片,這些碎片往往都比較小,無法被分配給新對象,因為新的對象需要大塊的連續內存空間。造成了嚴重的內存浪費。


回收完對象后不進行壓縮,導致出現的內存碎片
觸發GC的時機:
1. 當試圖分配內存,但失敗的時候
2. 當堆空間的大小達到了某些閾值的時候
3. 程序主動請求GC
DalvikGC的具體回收過程,總共分四個階段:
1.查找所有Root Sets(簡單理解為Root對象):GC會暫停所有線程,然后開始查找Root sets,Root sets一般指局部變量,thread對象,靜態變量等。或者說,Root sets就是你的應用可訪問到的,活着的對象。這一步會比較耗時,而且期間你的App會暫停運行。
2.標記可達對象(一):這個階段,GC會把剛找到的Root sets標記為可達(reachable),剩余的就是不可達對象,等待后續階段回收。標記可達的這一階段是並發執行的,也就是在這期間你的App會恢復運行。但是這個並發帶來了新問題,因為標記的同時又在給新的對象分配內存。
3.標記可達對象(二):給新的對象分配內存的時候,GC又要重新查找Root sets,執行第1步操作,暫停你的App。這個時候你的App會顯得卡頓,感覺上像是手機在執行什么高負荷的任務。 (原文是這樣寫的,個人覺得應該是上一步的用戶程序並發運行導致標記的結果可能會被修改,所以需要暫停線程,然后進行增量的標記更新)
4.回收:GC把所有沒有被標記為可達的對象進行回收。這一階段是並發執行的。


簡單示意
還記得KitKat那時候logcat打出來的那些GC_FOR_ALLOC信息嗎?那些日志就是在給新對象分配內存失敗的時候,GC啟動垃圾回收時打印的信息,如果完成了垃圾回收,發現內存還是不夠分配,可能會發生2種情況:
1.堆內存變大(如果當前還沒到最大值的話)
2.發生OOM
通常在給大對象分配內存的時候會發生這個現象,比如Bitmap。
ART
從Lollipop這個版本開始,谷歌用ART徹底替換了Dalvik。ART大幅提升了性能,包括采用預編譯(Ahead-Of-Time Compilation)機制,移除整個JIT編譯器和解釋器這樣的革命性改變。同時當然也對GC做了很多的改進。
Lollipop&Marshmallow
內存分配/回收的算法依然是CMS(並發標記-清除),但是優化了很多地方。
1. 最明顯的改動,就是用RosAlloc代替舊的dlmalloc作為內存分配的算法。在native代碼中調用malloc或在Java/C++中使用new關鍵字創建對象時都會用到RosAlloc算法。該算法的優點在於可以針對特定線程進行內存分配,這個優點給之后Oreo版本的進一步優化提供了基礎。
2. 第二個明顯的提升,是把小塊的內存分配進行分組合並,並在分配大塊內存時進行頁對齊。之所以能做到按頁對齊,是因為ART沒有對內存進行限制。不像Dalvik最多允許一個進程申請36MB的內存(不同設備的具體數值可能不同),ART對單個進程的內存上限沒有做限制。(這個說法。。應該不太准確,ActivityManager的getMemoryClass方法可以返回應用的內存閾值)
3. 另一個進步是GC終於支持了前台App的堆內存壓縮。這樣前台的App在需要的時候也可以釋放部分內存,降低OOM的概率,減少內存碎片,減少因內存碎片而導致的可用內存不足引發的GC,大幅提升了整體性能。
4. 這一版的GC使用的更細粒度的鎖,改善了在最終回收前的那一次停頓(細粒度鎖,阻塞的代碼更少了)。同時也把第一步的查找Root sets改為並發執行,這樣整體上,把運行一次標記-清除的回收操作的耗時從10ms左右減少到3ms左右。
5. 引入分代回收的機制。每次major gc完成后,垃圾回收器會追蹤之后每一個分配的對象,按照各自的存活時間和大小,對這些對象進行“分代”,對象一開始會被放到年輕代區域,存活時間夠長的話會被轉移到老年代,更“大齡”的放到永久代區域。這個優化對性能的提升是最明顯的。堆內存中每個“代”區域都有各自的內存上限,當達到上限的時候,系統會在這個區域發起一次gc,gc的耗時取決於該區域是哪一代,以及區域內有多少活躍的對象。
分代回收的引入,節省了內存分配(失敗)過程的耗時,因為在內存不足的情況下,會針對不同的區域,只在必要的情況下進行內存回收,比如如果在年輕代可以回收到足夠的內存,就直接完成分配,而不需要關心進一步去看老年代和永久代的情況,節省時間。
Nougat
這個版本主要的改動,就是用匯編語言重寫了內存分配過程,內存分配整體性能對比KitKat時期提升10倍。
Oreo
里程碑式的革新,重寫了整個垃圾回收器。新的GC名叫“並發堆壓縮回收器(Concurrent Heap Compaction Collector)”。
新版本的GC采用了“激進式碎片整理(always-defragmentation)”的策略,來解決堆壓縮的問題。即使App在前台,也盡可能及時地觸發內存壓縮。這個策略對整個設備的總的內存占用都有明顯的改善,因為無論是前台的應用包括系統服務,系統UI,媒體服務等,或是后台的其他服務,都可以更頻繁的進行堆內存壓縮(整理)。根據谷歌的說法,采用激進式的碎片整理策略能夠節省30%的總內存占用。
新GC的並發壓縮(Concurrent Compaction)同樣是一個很大的進步。Oreo版本的GC將堆內存分割為一個一個的桶(bucket),大小為256K,用來給對象分配內存。當某個線程需要內存的時候,GC會給該線程分配一個桶大小的內存區域,這樣該線程就可以在這個桶的區域內單獨進行分配和回收,而不需要鎖住整個系統。
同時,桶機制引出了另外一個改進之處:每當對一個桶的區域進行內存壓縮后,如果發現內存占用率不足70%或75%,這個桶會被直接回收掉,桶內的對象會被移動到另外一個桶里。(整體思路和G1垃圾回收器有點像)
另外,前面提到的Lollipop版本引入的RosAlloc分配算法,結合Oreo版本引入的指針碰撞(bump-the-pointer)技術,形成了一個新的機制——線程局部緩沖分配器(thread local bump-allocator)。這項技術使得Oreo版本的GC能夠在內存分配性能上領先Dalvik18倍之多,相比Nougat版本也提升了70%的性能。怎么做到的?
采用指針碰撞技術,在每個桶內有一個指針,指示了當前桶內最后一個元素的地址。在給一個新的對象分配內存的時候,GC會先給對象所在線程分配一個桶,然后通過桶的指針,判斷當前桶是否還有足夠的空間分給這個新的對象,如果有的話,直接完成內存分配,並將指針更新為這個新對象的地址。
但是Oreo版本的GC有一個退步的地方,就是GC的分代管理機制被去掉了。
Android 10(Q)
Android Q帶來了比以往更高性能的GC,同時分代GC回歸,並且保留了Oreo GC的所有特性。
總結
可以看到,經過多個版本的迭代,Android GC已經變得更成熟更健壯,內存的分配算法相比Dalvik時代更加的高校,內存回收也做到了更細粒度,回收期間僅需要鎖住當前的線程,而不是整個VM。
在Oreo版本,Android引入的激進式內存壓縮策略,通過優化那些常駐前台,很少進入后台的進程的內存占用,使得整個設備的內存使用情況有了大幅的改善。
原文: