垃圾回收器比較: 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 gc
和Mixed 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
的時候掃描狀態是dirty
的Card
即可。這是基本的用法,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對應一個Card
,young 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 Failure
和Concurrent 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。