這三大特性,讓 G1 取代了 CMS!

2022-08-29 12:01:34

大家好,我是樹哥。

之前我們聊過 CMS 回收器,但那時候我們說 CMS 回收器已經落伍了,現在應該是用 G1 回收器的時候了。那麼 G1 回收器到底有什麼魔力,它比 CMS 回收器相比強在哪裡呢?今天,就讓樹哥帶大家盤一盤!

G1 回收器的歷史

G1(Garbage-First)回收器早在 JDK1.7 的時候就確定要做,但直到 JDK7u4 的時候才正式推出使用。等到了 JDK9 之後變成了預設的垃圾回收器,同時廢棄了 CMS 回收器。

G1 回收器特性

G1 回收器是一款面向伺服器端應用的垃圾回收器,它的長期實名是替換 CMS 回收器。

G1 回收器於 CMS 回收器相比,它們有相似的地方,例如:都是關注 GC 停頓時間的回收器,都採用了分代回收的思想。但從整體的實現上來看,G1 回收器做了非常多的改進,可以說是對 CMS 回收器的全面改進。相對於 CMS 回收器來說,G1 回收器有下面幾個不同的地方:

  1. 採用化整為零的分割區思想
  2. 採用標記-整理的垃圾回收演演算法
  3. 可預測的 GC 停頓時間

分割區思想

對於 CMS 及之前的回收器來說,其 JVM 記憶體空間按照分代的思路劃分成物理連續的一大片區域,如下圖所示。

但在 G1 回收器中,雖然也採用了分代的思路,但其並沒有為其分配一塊連續的記憶體,而是將整塊記憶體化整為零拆分成一個個 Region,如下圖所示。

正如上圖所示,G1 回收器不再為年輕代和老年代劃分大塊的記憶體,而是劃分成了一個個的 Region,每個 Region 被標記成年輕代或者老年代。在 G1 中,還多了一個 Humongous 區域,其是為了優化大物件的分配而誕生的。

G1 回收器化整為零的 Region 設計思想,是 G1 回收器比 CMS 回收器強大的核心。 通過將大塊的記憶體化整為零,G1 回收器能夠更加靈活地控制 GC 停頓時間,並且也解決了 CMS 回收器存在的記憶體碎片問題以及大記憶體下的長 GC 停頓時間問題。

標記-整理演演算法

G1 回收器與 CMS 回收器的另一個很大的區別是:G1 回收器使用的是「標記-整理」演演算法,而 CMS 回收器使用的是「標記-清除」演演算法。 因此,CMS 回收器會產生非常多的記憶體碎片,而 G1 回收器則沒有這個困擾。

有些小夥伴會問:那為什麼 CMS 回收器不用「標記-整理」演演算法呢?

很簡單,因為 CMS 回收器的老年代很大,使用「標記-整理」演演算法需要耗費很長的 GC 停頓時間,這會導致介面響應時間變長。實際上 CMS 回收器後續提供了 -XX:+UseCMSCompactAtFullCollection 引數去實現記憶體壓縮,但在記憶體壓縮的時候 GC 停頓時間會很長,從而導致介面響應時間變長。

好奇寶寶又問了:G1 回收器也用的是「標記-整理」演演算法,為啥就不會導致長 GC 停頓時間呢?

很簡單,因為 G1 回收器使用了分 Region 的思想,其將大塊的記憶體化整為零成為 Region。此外,其還維護了一個待回收 Region 列表,可以選擇回收價效比最高的 Region 進行回收,從而實現對 GC 停頓時間的靈活控制。

看到了麼,G1 回收器化整為零的 Region 設計思想,真的是 G1 回收器的大殺器!

可預測的停頓時間

G1 回收器對於 CMS 而言還有一個很大的優勢,即能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒。對於該特性現在還用得比較少,大家瞭解一下就可以了。

垃圾回收過程

比起 CMS 回收器來說,G1 回收器的垃圾回收過程就比較特別了,其採用了「年輕代收集」和「混合收集」兩種垃圾回收方式。

年輕代收集

在應用剛剛啟動的時候,流量慢慢進來,JVM 開始生成物件。G1 會選擇一個分割區並指定 eden 分割區,當這塊分割區用滿之後,G1 會選一個新的分割區作為 eden 分割區。這個操作會一直進行下去,一直到達到 eden 分割區的上限,接著觸發一次年輕代收集。

年代收集採用的是「複製演演算法」,其首先使用單 eden、雙 survivor 遷移存活物件。在遷移過程中,會根據物件年齡以及其他特性,將物件晉升到老年代分割區中,原有的年輕代分割區會被整個回收掉。這個過程涉及到的規則和 CMS 回收器類似,只是 G1 回收器將記憶體化整為零了而已。

混合收集

隨著時間推移,越來越多的物件晉升到老年代中。當老年代佔比(佔 Java 堆記憶體的比例)達到 InitiatingHeapOccupancyPercent 引數之後,JVM 便會觸發「混合收集」進行垃圾收集。要注意的是:混合收集會收集年輕代和部分老年代的記憶體,其並不等同於 Full GC。Full GC 會回收整個老年代記憶體。

對於混合收集方式來說,其收集過程可以分為 4 個階段:

  • 初始標記
  • 並行標記
  • 最終標記
  • 篩選回收

初始標記。 該階段與 CMS 回收器一樣,都只是簡單標記一下 GC Roots 能直接關聯到的物件,讓後續 GC 回收執行緒能與使用者執行緒並行執行。初始標記階段是需要「Stop the World」的。

並行標記。 該階段與 CMS 回收器一樣,它從 GC Root 開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時很長,但可與使用者程式並行執行,不需要「Stop the World」。

最終標記。 該階段與 CMS 回收器一樣,它是為了修正在並行標記期間因使用者程式繼續運作而導致參照發生變化的問題。只是 G1 回收器採用了不同的方式去實現,在這個階段是需要「Stop the World」的。

篩選回收。 該階段與 CMS 回收器的並行清除一樣,它是去將標記為垃圾的物件清除掉。只是對於 G1 回收器來說,它會維護各個 Region 的回收價值和成本,隨後根據使用者期望的 GC 停頓時間來指定回收計劃。

整體看下來,我們會發現 G1 回收器的混合收集過程與 CMS 回收器非常類似,都經歷初始標記、並行標記、最終標記、篩選回收(並行清除)幾個階段。

總結

從 JDK7 正式推出到 JDK9 成為預設的垃圾收集器,G1 回收器用了兩代人的時間打敗了 CMS 回收器。

從 G1 回收器的實現來看,其開創性的化整為零的 Region 設計思想,無疑是其打敗 CMS 回收器的祕訣。通過該設計思想,G1 回收器得以更加靈活地控制 GC 停頓時間,同時也可以實現更加高效、複雜的功能,例如:根據回收空間和耗時選擇最佳的回收 Region、預測 GC 停頓時間等。

參考資料