深入淺出 Java 虛擬機(五)G1 垃圾回收器

本文章爲《深入淺出 Java 虛擬機》系列課程學習筆記,侵刪。學習地址爲 深入淺出 Java 虛擬機

1 CMS 垃圾回收器存在的問題

以下場景很極端,但卻經常發生。

在發生 Minor GC 時,由於 Survivor 區已經放不下了,多出的對象只能提升(promotion)到老年代。但是此時老年代因爲空間碎片的緣故,會發生 concurrent mode failure 的錯誤。這個時候,就需要降級爲 Serail Old 垃圾回收器進行收集。這就是比 concurrent mode failure 更加嚴重的 promotion failed 問題。

一次簡單的 Major GC,竟然能演化成耗時最長的 Full GC。最要命的是,這個停頓時間是不可預知的。

有沒有一種辦法,能夠首先定義一個停頓時間,然後反向推算收集內容呢?有滴,G1 垃圾回收器瞭解一下,它不要求每次都把垃圾清理的乾乾淨淨,它只是努力做它認爲對的事情。

我們要求 G1,在任意 1 秒的時間內,停頓不得超過 10ms,這就是在給它制定一個目標。G1 會盡量達成這個目標,它能夠推算出本次要收集的大體區域,以增量的方式完成收集。

這也是使用 G1 垃圾回收器不得不設置的一個參數:-XX:MaxGCPauseMillis=10

2 什麼是 G1 垃圾回收器?

G1 垃圾回收器用於取代 CMS 垃圾回收器。

G1 垃圾回收器與其他的垃圾回收器在對堆的劃分上有一些不同。其他的回收器,都是對某個年代的整體收集,收集時間上自然不好控制。G1 把堆切成了很多份,把每一份當作一個小目標,部分上目標很容易達成。

那麼,G1 有年輕代和老年代的區分嗎?

在這裏插入圖片描述
如圖所示,G1 也是有 Eden 區和 Survivor 區的概念的,只不過它們在內存上不是連續的,而是由一小份一小份組成的。

這一小份區域的大小是固定的,名字叫作小堆區(Region)。小堆區可以是 Eden 區,也可以是 Survivor 區,還可以是 Old 區。所以 G1 的年輕代和老年代的概念都是邏輯上的。

每一塊 Region,大小都是一致的,它的數值是在 1M 到 32M 字節之間的一個 2 的冪值數。

但假如我的對象太大,一個 Region 放不下了怎麼辦?注意圖中有一塊麪積很大的黃色區域,它的名字叫作 Humongous Region,大小超過 Region 50% 的對象,將會在這裏分配。

那麼,回收的時候,到底回收哪些小堆區呢?是隨機的麼?這當然不是。事實上,垃圾最多的小堆區,會被優先收集。這就是 G1(Garbage­First GC)名字的由來。

3 G1 的垃圾回收過程

在邏輯上,G1 分爲年輕代和老年代,但它的年輕代和老年代比例,並不是那麼“固定”,爲了達到 MaxGCPauseMillis 所規定的效果,G1 會自動調整兩者之間的比例。

G1 的回收過程主要分爲 3 類:

  1. G1“年輕代”的垃圾回收,同樣叫 Minor GC,這個過程和我們前面描述的類似,發生時機就是 Eden 區滿的時候
  2. 老年代的垃圾收集,嚴格上來說其實不算是收集,它是一個“併發標記”的過程,順便清理了一點點對象
  3. 真正的清理,發生在“混合模式”,它不止清理年輕代,還會將老年代的一部分區域進行清理

RSet

RSet 是一個空間換時間的數據結構。

之前我們提到過一個叫作卡表(CardTable)的數據結構,用來解決跨代引用的問題。RSet 的功能與此類似,它的全稱是 RememberedSet,用於記錄和維護 Region 之間的對象引用關係。

但 RSet 與 Card Table 有些不同的地方。Card Table 是一種 points-out(我引用了誰的對象)的結構。而 RSet 記錄了其他 Region 中的對象引用本 Region 中對象的關係,屬於 points-into 結構(誰引用了我的對象),有點倒排索引的味道。

你可以把 RSet 理解成一個 Hash,key 是引用的 Region 地址,value 是引用它的對象的卡頁集合。

有了這個數據結構,在回收某個 Region 的時候,就不必對整個堆內存的對象進行掃描了。它使得部分收集成爲了可能。

對於年輕代的 Region,它的 RSet 只保存了來自老年代的引用,這是因爲年輕代的回收是針對所有年輕代 Region 的,沒必要畫蛇添足。所以說年輕代 Region 的 RSet 有可能是空的。

而對於老年代的 Region 來說,它的 RSet 也只會保存老年代對它的引用。這是因爲老年代回收之前,會先對年輕代進行回收。這時,Eden 區變空了,而在回收過程中會掃描 Survivor 分區,所以也沒必要保存來自年輕代的引用。

RSet 通常會佔用很大的空間,大約 5% 或者更高。不僅僅是空間方面,很多計算開銷也是比較大的。

具體回收過程

G1 還有一個 CSet 的概念。它的全稱是 Collection Set,即收集集合,保存一次 GC 中將執行垃圾回收的區間(Region)。GC 是在 CSet 中的所有存活數據(Live Data)都會被轉移。

年輕代回收

年輕代回收是一個 STW 的過程,它的跨代引用使用 RSet 數據結構來追溯,會一次性回收掉年輕代的所有 Region。

JVM 啓動時,G1 會先準備好 Eden 區,程序在運行過程中不斷創建對象到 Eden 區,當所有的 Eden 區都滿了,G1 會啓動一次年輕代垃圾回收過程。

年輕代的收集包括下面的回收階段:

  1. 掃描根:根,可以看作是我們前面介紹的 GC Roots,加上 RSet 記錄的其他 Region 的外部引用
  2. 更新 RS:處理 dirty card queue 中的卡頁,更新 RSet。此階段完成後,RSet 可以準確的反映老年代對所在的內存分段中對象的引用。可以看作是第一步的補充
  3. 處理 RS:識別被老年代對象指向的 Eden 中的對象,這些被指向的 Eden 中的對象被認爲是存活的對象
  4. 複製對象:沒錯,收集算法依然使用的是 Copy 算法。在這個階段,對象樹被遍歷,Eden 區內存段中存活的對象會被複制到 Survivor 區中空的 Region。這個過程和其他垃圾回收算法一樣,包括對象的年齡和晉升
  5. 處理引用:處理 Soft、Weak、Phantom、Final、JNI Weak 等引用。結束收集
併發標記

當整個堆內存使用達到一定比例(默認是 45%),併發標記階段就會被啓動。這個比例也是可以調整的,通過參數 -XX:InitiatingHeapOccupancyPercent 進行配置。

Concurrent Marking 是爲 Mixed GC 提供標記服務的,並不是一次 GC 過程的一個必須環節。具體標記過程如下:

  1. 初始標記:這個過程共用了 Minor GC 的暫停,這是因爲它們可以複用 root scan 操作。雖然是 STW 的,但是時間通常非常短
  2. Root 區掃描
  3. 併發標記:這個階段從 GC Roots 開始對 heap 中的對象標記,標記線程與應用程序線程並行執行,並且收集各個 Region 的存活對象信息
  4. 重新標記:和 CMS 類似,也是 STW 的。標記那些在併發標記階段發生變化的對象
  5. 清理階段:這個過程不需要 STW。如果發現 Region 裏全是垃圾,在這個階段會立馬被清除掉。不全是垃圾的 Region,並不會被立馬處理,它會在 Mixed GC 階段,進行收集
SATB

如果在併發標記階段,又有新的對象變化,該怎麼辦?這是由算法 SATB 保證的。

SATB 即 Snapshot At The Beginning,用於保證在併發標記階段的正確性。
在這裏插入圖片描述
這個快照是邏輯上的,主要是有幾個指針,將 Region 分成個多個區段。如圖所示,併發標記期間分配的對象,都會在 next TAMS 和 top 之間。

混合回收(Mixed GC)

能併發清理老年代中的整個整個的小堆區是一種最優情形。混合收集過程,不只清理年輕代,還會將一部分老年代區域也加入到 CSet 中。

通過 Concurrent Marking 階段,我們已經統計了老年代的垃圾佔比。在 Minor GC 之後,如果判斷這個佔比達到了某個閾值,下次就會觸發 Mixed GC。這個閾值,由 -XX:G1HeapWastePercent 參數進行設置(默認是堆大小的 5%)。因爲這種情況下, GC 會花費很多的時間但是回收到的內存卻很少。所以這個參數也是可以調整 Mixed GC 的頻率的。

還有參數 G1MixedGCCountTarget,用於控制一次併發標記之後,最多執行 Mixed GC 的次數。

4 ZGC

G1 可能存在的問題

在系統切換到 G1 垃圾回收器之後,線上發生的嚴重 GC 問題已經非常少了,這歸功於 G1 的預測模型和它創新的分區模式。但預測模型也會有失效的時候,它並不是總如我們期望的那樣運行,尤其是你給它定下一個苛刻的目標之後。

另外,如果應用的內存非常喫緊,對內存進行部分回收根本不夠,始終要進行整個 Heap 的回收,那麼 G1 要做的工作量就一點也不會比其他垃圾回收器少,而且因爲本身算法複雜了,還可能比其他回收器要差。

ZGC 的特點

  1. 停頓時間不會超過 10ms
  2. 停頓時間不會隨着堆的增大而增大(不管多大的堆都能保持在 10ms 以下)
  3. 可支持幾百 M,甚至幾 T 的堆大小(最大支持 4T)

在 ZGC 中,連邏輯上的年輕代和老年代也去掉了,只分爲一塊塊的 page,每次進行 GC 時,都會對 page 進行壓縮操作,所以沒有碎片問題。ZGC 還能感知 NUMA 架構,提高內存的訪問速度。與傳統的收集算法相比,ZGC 直接在對象的引用指針上做文章,用來標識對象的狀態,所以它只能用在 64 位的機器上。

現在在線上使用 ZGC 的還非常少。即使是用,也只能在 Linux 平臺上使用。等待它的普及,還需要一段時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章