1. 程式人生 > >垃圾回收器比較: G1 vs CMS

垃圾回收器比較: G1 vs CMS

1. 分代收集

這個現在是垃圾回收器的標配,G1和CMS也不例外。但是G1同時回收老年代和年輕代,而CMS只能回收老年代,需要配合一個年輕代收集器。另外G1的分代更多是邏輯上的概念,G1將記憶體分成多個等大小的region,Eden/ Survivor/Old分別是一部分region的邏輯集合,物理上記憶體地址並不連續。

G1邏輯分代


CMS在old gc的時候會回收整個Old區,對G1來說沒有old gc的概念,而是區分Fully young gcMixed gc,前者對應年輕代的垃圾回收,後者混合了年輕代和部分老年代的收集,因此每次收集肯定會回收年輕代,老年代根據記憶體情況可以不回收或者回收部分或者全部(這種情況應該是可能出現)。

2. 如何處理跨代引用

在垃圾回收的時候都是從Root開始搜尋,這會先經過年輕代再到老年代,對於年輕代引用老年代的這種跨代不需要單獨處理。但是老年代引用年輕代的會影響young gc,這種跨代需要處理。
為了避免在回收年輕代的時候掃描整個老年代,需要記錄老年代對年輕代的引用,young gc的時候只要掃描這個記錄。CMS和G1都用到了Card Table,但是用法不太一樣。JVM將記憶體分成一個個固定大小的card,然後有一個專門的資料結構(即這裡的Card Table)維護每個Card的狀態,一個位元組對應一個Card,有點像記憶體page的概念,只是page是硬體上的,Card Table

是軟體上的。當一個Card上的物件的引用發生變化的時候,就將這個Card對應的Card Table上的狀態置為dirty,young gc的時候掃描狀態是dirtyCard即可。這是基本的用法,CMS基本上就是這麼使用。
G1在Card Table的基礎上引入的remembered set(下面簡稱RSet)。每個region都會維護一個RSet,記錄著引用到本region中的物件的其他region的Card。比如A物件在regionA,B物件在regionB,且B.f = A,則在regionA的RSet中需要記錄B所在的Card的地址。這樣的好處是可以對region進行單獨回收,這要求RSet不只是維護老年代到年輕代的引用,也要維護這老年代到老年代的引用,對於跨代引用的每次只要掃描這個region的RSet上的Card
即可。
上面說過年輕代到老年代的引用不需要單獨處理,這帶來了很大的效能上的提升,因為年輕代的物件引用變化很大,如果都需要記錄下來成本會很高。同時也說明只需要在老年代維護Card Table

3. 如何處理併發過程的物件變化

CMS和G1都有併發處理過程,這個過程應用程式跟著gc執行緒一起執行,會產生新物件,也會有舊的物件死去,物件之間的引用關係也會發生變化。這部分資料可以暫時不處理,留到下一次再處理嗎?如果可以這樣的話問題就會變得很簡單,但是答案是不行。考慮下圖的場景(圖中每一行表示一個記憶體狀態,每一列表示一個Card,這裡有4個):第一步a是併發標記中途的一個狀態,標記了a b c e四個物件,0 1兩個Card已經標記好;第二步b併發標記的同時引用發生變化,g不再指向d,而b不再指向c,變成指向d,這個時候處理Card 2,會標記到g,然後就標記結束了,導致d物件丟失。

image.png

CMS初始標記的時候會標記所有從root直接可達的物件,併發標記的時候再從這些物件進一步搜尋其他可達物件,最終構成一個存活的物件圖。併發標記過程中引用發生變化的也是通過Card Table來記錄。但是young gc的時候如果一個dirty card沒有包含到年輕代的引用,這個card會重新標記為clean,這有可能將併發標記過程產生的dirty card錯誤清除,因此CMS引入了另一個數據結構mod union table,這裡一個bit對應一個Cardyoung gc在將Card Table設定為clean的時候會將對應的mod union table置為dirty。最終標記的時候會將Card Table或者mod union table是dirty的Card也作為root去掃描,從而解決併發標記過程產生的引用變化。CMS還需要處理併發過程從年輕代晉升到老年代的物件,處理方式是將這部分物件也作為root去掃描。
G1使用一個稱為snapshot at the beginning(下面簡稱SATB)的演算法,在初始標記的時候得到一個從root直接可達的snapshot,之後從這個snapshot不可達的物件都是可以回收的垃圾,併發過程產生的物件都預設是活的物件,留到下一次再處理。對於引用關係發生變化的,將這個物件對應的Card放到一個SATB佇列裡,在最終標記的時候進行處理(如果超過一定的閾值併發標記的時候也會處理一部分),處理的過程就是以佇列中的Card作為root進行掃描。

4. Write Barrier

Write Barrier可以理解為在寫的時候插入一條特定的操作。
在CMS中老年代引用年輕代的時候就是通過觸發一個Write Barrier來更新Card Table的標誌位。這是一個同步操作,在更新引用的時候順帶執行,只需要兩個指令,引入的消耗不大。
G1比較複雜,在兩個地方用到了Write Barrier,分別是更新RSet的rememberd set Write Barrier和記錄引用變化的Concurrent Marking Write Barrier,前者發生在引用更新之後,稱為Post Write Barrier,後者發生在引用變化之前,稱為Pre Write Barrier。G1為了提高效能,這兩個Write Barrier都是先放到佇列中,再非同步進行處理。具體可以參考Garbage-First Garbage Collection 論文筆記

5. Full GC

導致CMS Full GC的可能原因主要有兩個:Promotion FailureConcurrent Mode Failure,前者是在年輕代晉升的時候老年代沒有足夠的連續空間容納,很有可能是記憶體碎片導致的;後者是在併發過程中jvm覺得在併發過程結束前堆就會滿了,需要提前觸發Full GC。CMS的Full GC是一個多執行緒STW的Mark-Compact過程,,需要儘量避免或者降低頻率。
G1的初衷就是要避免Full GC的出現,Full GC會會對所有region做Evacuation-Compact,而且是單執行緒的STW,非常耗時間。導致G1 Full GC的原因可能有兩個:1. Evacuation的時候沒有足夠的to-space來存放晉升的物件;2. 併發處理過程完成之前空間耗盡。這兩個原因跟CMS類似。



作者:searchworld
連結:https://www.jianshu.com/p/bdd6f03923d1
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。