二 垃圾回收:第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?

本課時我們主要來看下這兩個高頻的面試考題:

  • G1 的回收原理是什麼?爲什麼 G1 比傳統 GC 回收性能好?
  • 爲什麼 G1 如此完美仍然會有 ZGC?

我們在上一課時,簡要的介紹了 CMS 垃圾回收器,下面我們簡單回憶一下它的一個極端場景(而且是經常發生的場景)。

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

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

有沒有一種辦法,能夠首先定義一個停頓時間,然後反向推算收集內容呢?就像是領導在年初制定 KPI 一樣,分配的任務多就多幹些,分配的任務少就少乾點。

很久之前就有領導教導過我,如果你列的目標太大,看起來無法完成,不要怕。有一個叫作里程碑的名詞,可以讓我們以小跑的姿態,完成一次馬拉松。

G1 的思路說起來也類似,它不要求每次都把垃圾清理的乾乾淨淨,它只是努力做它認爲對的事情。

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

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

-XX:MaxGCPauseMillis=10

爲什麼叫 G1

G1 的目標是用來幹掉 CMS 的,它同樣是一款軟實時垃圾回收器。相比 CMS,G1 的使用更加人性化。比如,CMS 垃圾回收器的相關參數有 72 個,而 G1 的參數只有 26 個。

G1 的全稱是 Garbage­First GC,爲了達成上面制定的 KPI,它和前面介紹的垃圾回收器,在對堆的劃分上有一些不同。

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

那又有一個面試題來啦:G1 有年輕代和老年代的區分嗎?

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

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

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

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

Region 的大小,可以通過參數進行設置:
-XX:G1HeapRegionSize=<N>M

那麼,回收的時候,到底回收哪些小堆區呢?是隨機的麼?

這當然不是。事實上,垃圾最多的小堆區,會被優先收集。這就是 G1 名字的由來。

G1 的垃圾回收過程

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

如果你強行使用 -Xmn 或者 -XX:NewRatio 去設定它們的比例的話,我們給 G1 設定的這個目標將會失效。

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

(1)G1“年輕代”的垃圾回收,同樣叫 Minor GC,這個過程和我們前面描述的類似,發生時機就是 Eden 區滿的時候。

(2)老年代的垃圾收集,嚴格上來說其實不算是收集,它是一個“併發標記”的過程,順便清理了一點點對象。

(3)真正的清理,發生在“混合模式”,它不止清理年輕代,還會將老年代的一部分區域進行清理。

在 GC 日誌裏,這個過程描述特別有意思,(1)的過程,叫作 [GC pause (G1 Evacuation Pause) (young),而(2)的過程,叫作 [GC pause (G1 Evacuation Pause) (mixed)。Evacuation 是轉移的意思,和 Copy 的意思有點類似。

這三種模式之間的間隔也是不固定的。比如,1 次 Minor GC 後,發生了一次併發標記,接着發生了 9 次 Mixed GC。

RSet

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

在第 6 課時中,我們提到過一個叫作卡表(Card Table)的數據結構,用來解決跨代引用的問題。RSet 的功能與此類似,它的全稱是 Remembered Set,用於記錄和維護 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% 或者更高。不僅僅是空間方面,很多計算開銷也是比較大的。

事實上,爲了維護 RSet,程序運行的過程中,寫入某個字段就會產生一個 post-write barrier 。爲了減少這個開銷,將內容放入 RSet 的過程是異步的,而且經過了很多的優化:Write Barrier 把髒卡信息存放到本地緩衝區(local buffer),有專門的 GC 線程負責收集,並將相關信息傳給被引用 Region 的 RSet。

參數 -XX:G1ConcRefinementThreads 或者 -XX:ParallelGCThreads 可以控制這個異步的過程。如果併發優化線程跟不上緩衝區的速度,就會在用戶進程上完成。

具體回收過程

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 等引用。結束收集。

它的大體示意圖如下所示。

併發標記(Concurrent Marking)

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

Concurrent Marking 是爲 Mixed GC 提供標記服務的,並不是一次 GC 過程的一個必須環節。這個過程和 CMS 垃圾回收器的回收過程非常類似,你可以類比 CMS 的回收過程看一下。具體標記過程如下:

(1)初始標記(Initial Mark)

這個過程共用了 Minor GC 的暫停,這是因爲它們可以複用 root scan 操作。雖然是 STW 的,但是時間通常非常短。

(2)Root 區掃描(Root Region Scan)

(3)併發標記( Concurrent Mark)

這個階段從 GC Roots 開始對 heap 中的對象標記,標記線程與應用程序線程並行執行,並且收集各個 Region 的存活對象信息。

(4)重新標記(Remaking)

和 CMS 類似,也是 STW 的。標記那些在併發標記階段發生變化的對象。

(5)清理階段(Cleanup)

這個過程不需要 STW。如果發現 Region 裏全是垃圾,在這個階段會立馬被清除掉。不全是垃圾的 Region,並不會被立馬處理,它會在 Mixed GC 階段,進行收集。

瞭解 CMS 垃圾回收器後,上面這個過程就比較好理解。但是還有一個疑問需要稍微提一下。

如果在併發標記階段,又有新的對象變化,該怎麼辦?

這是由算法 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 的次數。

ZGC

你有沒有感覺,在系統切換到 G1 垃圾回收器之後,線上發生的嚴重 GC 問題已經非常少了?

這歸功於 G1 的預測模型和它創新的分區模式。但預測模型也會有失效的時候,它並不是總如我們期望的那樣運行,尤其是你給它定下一個苛刻的目標之後。

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

所以垃圾回收器本身的優化和升級,從來都沒有停止過。最新的 ZGC 垃圾回收器,就有 3 個令人振奮的 Flag:

    1. 停頓時間不會超過 10ms;

    2. 停頓時間不會隨着堆的增大而增大(不管多大的堆都能保持在 10ms 以下);

    3. 可支持幾百 M,甚至幾 T 的堆大小(最大支持 4T)。

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

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

小結

本課時,我們簡要看了下 G1 垃圾回收器的回收過程,並着重看了一下底層的數據結構 RSet。基本思想很簡單,但實現細節卻特別多。這不是我們的重點,對 G1 詳細過程感興趣的,可以參考紙質書籍。我也會通過其他途徑分享一些細節,你也可以關注拉勾教育公衆號後進學習羣與大家一起多多交流。

相對於 CMS,G1 有了更可靠的駕馭度。而且有 RSet 和 SATB 等算法的支撐,Remark 階段更加高效。

G1 最重要的概念,其實就是 Region。它採用分而治之,部分收集的思想,盡力達到我們給它設定的停頓目標。

G1 的垃圾回收過程分爲三種,其中,併發標記階段,爲更加複雜的 Mixed GC 階段做足了準備。

以下是一個線上運行系統的 JVM 參數樣例。這些參數,現在你都能看懂麼?如果有問題可以在評論區討論。

JAVA_OPTS="$JAVA_OPTS -XX:NewRatio=2 -XX:G1HeapRegionSize=8m -XX:MetaspaceSize
=256m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=10 -XX:+UseG1GC
 -XX:InitiatingHeapOccupancyPercent=45 -XX:MaxGCPauseMillis=200 -verbose:gc
  -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC
   -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=6
    -XX:GCLogFileSize=32m -Xloggc:./var/run/gc.log.$(date +%Y%m%d%H%M)
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./var/run/heap-dump.hprof
      -Dfile.encoding=UTF-8 -Dcom.sun.management.jmxremote -Dcom.sun.management.
      jmxremote.port=${JMX_PORT:-0} -Dcom.sun.management.jmxremote.ssl=false
       -Dcom.sun.management.jmxremote.authenticate=false"

 

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