(七)详解G1GC
了解CMS之后我們簡單回憶一下它的一個極端場景(而且是經常發(fā)生的場景)。
在發(fā)生 Minor GC 時,由于 Survivor 區(qū)已經放不下了,多出的對象只能提升(promotion)到老年代。但是此時老年代因為空間碎片的緣故,會發(fā)生 concurrent mode failure 的錯誤。這個時候,就需要降級為 Serail Old 垃圾回收器進行收集。這就是比 concurrent mode failure 更加嚴重的 promotion failed 問題。
?
一次簡單的 Major GC,竟然能演化成耗時最長的 Full GC。最要命的是,這個停頓時間是不可預知的。
有沒有一種辦法,能夠首先定義一個停頓時間,然后反向推算收集內容呢?就像是領導在年初制定 KPI 一樣,分配的任務多就多干些,分配的任務少就少干點。
我們要求 G1,在任意 1 秒的時間內,停頓不得超過 10ms。G1 會盡量達成這個目標,它能夠推算出本次要收集的大體區(qū)域,以增量的方式完成收集。
這也是使用 G1 垃圾回收器不得不設置的一個參數(shù):
-XX:MaxGCPauseMillis=10
為什么叫 G1
G1 是用來干掉 CMS 的,它同樣是一款軟實時垃圾回收器。相比 CMS,G1 的使用更加人性化。比如,CMS 垃圾回收器的相關參數(shù)有 72 個,而 G1 的參數(shù)只有 26 個。
G1 的全稱是 Garbage-First GC,為了達成上面制定的 KPI,它和前面介紹的垃圾回收器,在對堆的劃分上有一些不同。
其他的回收器,都是對某個年代的整體收集,收集時間上自然不好控制。G1 把堆切成了很多份,把每一份當作一個小目標,部分上目標很容易達成。
那又有一個面試題來啦:G1 有年輕代和老年代的區(qū)分嗎?
如圖所示,G1 也是有 Eden 區(qū)和 Survivor 區(qū)的概念的,只不過它們在內存上不是連續(xù)的,而是由一小份一小份組成的。且區(qū)域的大小是固定的,它的數(shù)值是在 1M 到 32M 字節(jié)之間的一個 2 的冪值數(shù)。
名字叫作小堆區(qū)(Region)。小堆區(qū)可以是 Eden 區(qū),也可以是 Survivor 區(qū),還可以是 Old 區(qū)。所以 G1 的年輕代和老年代的概念都是邏輯上的。
但假如我的對象太大,一個 Region 放不下了怎么辦?注意圖中有一塊面積很大的黃色區(qū)域,它的名字叫作 Humongous Region,大小超過 Region 50% 的對象,將會在這里分配。
Region 的大小,可以通過參數(shù)進行設置:
-XX:G1HeapRegionSize=<N>M
那么,回收的時候,到底回收哪些小堆區(qū)呢?是隨機的么?這當然不是。事實上,垃圾最多的小堆區(qū),會被優(yōu)先收集。這就是 G1 名字的由來。
?
G1 的垃圾回收過程
在邏輯上,G1 分為年輕代和老年代,但它的年輕代和老年代比例,并不是那么“固定”,為了達到 MaxGCPauseMillis 所規(guī)定的效果,G1 會自動調整兩者之間的比例。
如果你強行使用 -Xmn 或者 -XX:NewRatio 去設定它們的比例的話,我們給 G1 設定的這個目標將會失效。
G1 的回收過程主要分為 3 類:
(1)G1“年輕代”的垃圾回收,同樣叫 Minor GC,這個過程和我們前面描述的類似,發(fā)生時機就是 Eden 區(qū)滿的時候。
(2)老年代的垃圾收集,嚴格上來說其實不算是收集,它是一個“并發(fā)標記”的過程,順便清理了一點點對象。
(3)真正的清理,發(fā)生在“混合模式”,它不止清理年輕代,還會將老年代的一部分區(qū)域進行清理。
在 GC 日志里,這個過程描述特別有意思,(1)的過程,叫作 [GC pause (G1 Evacuation Pause) (young),而(2)的過程,叫作 [GC pause (G1 Evacuation Pause) (mixed)。Evacuation 是轉移的意思,和 Copy 的意思有點類似。
這三種模式之間的間隔也是不固定的。比如,1 次 Minor GC 后,發(fā)生了一次并發(fā)標記,接著發(fā)生了 9 次 Mixed GC。
?
RSet
RSet 是一個空間換時間的數(shù)據(jù)結構。
分代回收的時候提出過一個叫作卡表(Card Table)的數(shù)據(jù)結構,用來解決跨代引用的問題。RSet 的功能與此類似,它的全稱是 Remembered Set,用于記錄和維護 Region 之間的對象引用關系。
但 RSet 與 Card Table 有些不同的地方。Card Table 是一種 points-out(我引用了誰的對象)的結構。而 RSet 記錄了其他 Region 中的對象引用本 Region 中對象的關系,屬于 points-into 結構(誰引用了我的對象),有點倒排索引的味道。
你可以把 RSet 理解成一個 Hash,key 是引用的 Region 地址,value 是引用它的對象的卡頁集合。
有了這個數(shù)據(jù)結構,在回收某個 Region 的時候,就不必對整個堆內存的對象進行掃描了。它使得部分收集成為了可能。
對于年輕代的 Region,它的 RSet 只保存了來自老年代的引用,這是因為年輕代的回收是針對所有年輕代 Region 的,沒必要畫蛇添足。所以說年輕代 Region 的 RSet 有可能是空的。
而對于老年代的 Region 來說,它的 RSet 也只會保存老年代對它的引用。這是因為老年代回收之前,會先對年輕代進行回收。這時,Eden 區(qū)變空了,而在回收過程中會掃描 Survivor 分區(qū),所以也沒必要保存來自年輕代的引用。
RSet 通常會占用很大的空間,大約 5% 或者更高。不僅僅是空間方面,很多計算開銷也是比較大的。
事實上,為了維護 RSet,程序運行的過程中,寫入某個字段就會產生一個 post-write barrier 。為了減少這個開銷,將內容放入 RSet 的過程是異步的,而且經過了很多的優(yōu)化:Write Barrier 把臟卡信息存放到本地緩沖區(qū)(local buffer),有專門的 GC 線程負責收集,并將相關信息傳給被引用 Region 的 RSet。
參數(shù) -XX:G1ConcRefinementThreads 或者 -XX:ParallelGCThreads 可以控制這個異步的過程。如果并發(fā)優(yōu)化線程跟不上緩沖區(qū)的速度,就會在用戶進程上完成。
?
具體回收過程
G1 還有一個 CSet 的概念。這個就比較好理解了,它的全稱是 Collection Set,即收集集合,保存一次 GC 中將執(zhí)行垃圾回收的區(qū)間(Region)。GC 是在 CSet 中的所有存活數(shù)據(jù)(Live Data)都會被轉移。
?
年輕代回收
年輕代回收是一個 STW 的過程,它的跨代引用使用 RSet 數(shù)據(jù)結構來追溯,會一次性回收掉年輕代的所有 Region。
JVM 啟動時,G1 會先準備好 Eden 區(qū),程序在運行過程中不斷創(chuàng)建對象到 Eden 區(qū),當所有的 Eden 區(qū)都滿了,G1 會啟動一次年輕代垃圾回收過程。
? ? ? ?
年輕代的收集包括下面的回收階段:
(1)掃描根
根,可以看作是我們前面介紹的 GC Roots,加上 RSet 記錄的其他 Region 的外部引用。
(2)更新 RS
處理 dirty card queue 中的卡頁,更新 RSet。此階段完成后,RSet 可以準確的反映老年代對所在的內存分段中對象的引用。可以看作是第一步的補充。
(3)處理 RS
識別被老年代對象指向的 Eden 中的對象,這些被指向的 Eden 中的對象被認為是存活的對象。
(4)復制對象
沒錯,收集算法依然使用的是 Copy 算法。
在這個階段,對象樹被遍歷,Eden 區(qū)內存段中存活的對象會被復制到 Survivor 區(qū)中空的 Region。這個過程和其他垃圾回收算法一樣,包括對象的年齡和晉升,無需做過多介紹。
(5)處理引用
處理 Soft、Weak、Phantom、Final、JNI Weak 等引用。結束收集。
它的大體示意圖如下所示。
并發(fā)標記(Concurrent Marking)
當整個堆內存使用達到一定比例(默認是 45%),并發(fā)標記階段就會被啟動。這個比例也是可以調整的,通過參數(shù) -XX:InitiatingHeapOccupancyPercent 進行配置。
Concurrent Marking 是為 Mixed GC 提供標記服務的,并不是一次 GC 過程的一個必須環(huán)節(jié)。這個過程和 CMS 垃圾回收器的回收過程非常類似,你可以類比 CMS 的回收過程看一下。具體標記過程如下:
(1)初始標記(Initial Mark)
這個過程共用了 Minor GC 的暫停,這是因為它們可以復用 root scan 操作。雖然是 STW 的,但是時間通常非常短。
(2)Root 區(qū)掃描(Root Region Scan)
(3)并發(fā)標記( Concurrent Mark)
這個階段從 GC Roots 開始對 heap 中的對象標記,標記線程與應用程序線程并行執(zhí)行,并且收集各個 Region 的存活對象信息。
(4)重新標記(Remaking)
和 CMS 類似,也是 STW 的。標記那些在并發(fā)標記階段發(fā)生變化的對象。
(5)清理階段(Cleanup)
這個過程不需要 STW。如果發(fā)現(xiàn) Region 里全是垃圾,在這個階段會立馬被清除掉。不全是垃圾的 Region,并不會被立馬處理,它會在 Mixed GC 階段,進行收集。
?
了解 CMS 垃圾回收器后,上面這個過程就比較好理解。但是還有一個疑問需要稍微提一下。
如果在并發(fā)標記階段,又有新的對象變化,該怎么辦?
這是由算法 SATB 保證的。SATB 的全稱是 Snapshot At The Beginning,它作用是保證在并發(fā)標記階段的正確性。
這個快照是邏輯上的,主要是有幾個指針,將 Region 分成個多個區(qū)段。如圖所示,并發(fā)標記期間分配的對象,都會在 next TAMS 和 top 之間。
?
混合回收(Mixed GC)
能并發(fā)清理老年代中的整個整個的小堆區(qū)是一種最優(yōu)情形。混合收集過程,不只清理年輕代,還會將一部分老年代區(qū)域也加入到 CSet 中。
通過 Concurrent Marking 階段,我們已經統(tǒng)計了老年代的垃圾占比。在 Minor GC 之后,如果判斷這個占比達到了某個閾值,下次就會觸發(fā) Mixed GC。這個閾值,由 -XX:G1HeapWastePercent 參數(shù)進行設置(默認是堆大小的 5%)。因為這種情況下, GC 會花費很多的時間但是回收到的內存卻很少。所以這個參數(shù)也是可以調整 Mixed GC 的頻率的。
還有參數(shù) G1MixedGCCountTarget,用于控制一次并發(fā)標記之后,最多執(zhí)行 Mixed GC 的次數(shù)。
?
ZGC
你有沒有感覺,在系統(tǒng)切換到 G1 垃圾回收器之后,線上發(fā)生的嚴重 GC 問題已經非常少了?
這歸功于 G1 的預測模型和它創(chuàng)新的分區(qū)模式。但預測模型也會有失效的時候,它并不是總如我們期望的那樣運行,尤其是你給它定下一個苛刻的目標之后。
另外,如果應用的內存非常吃緊,對內存進行部分回收根本不夠,始終要進行整個 Heap 的回收,那么 G1 要做的工作量就一點也不會比其他垃圾回收器少,而且因為本身算法復雜了,還可能比其他回收器要差。
所以垃圾回收器本身的優(yōu)化和升級,從來都沒有停止過。最新的 ZGC 垃圾回收器,就有 3 個令人振奮的 Flag:
停頓時間不會超過 10ms;
停頓時間不會隨著堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
可支持幾百 M,甚至幾 T 的堆大小(最大支持 4T)。
在 ZGC 中,連邏輯上的年輕代和老年代也去掉了,只分為一塊塊的 page,每次進行 GC 時,都會對 page 進行壓縮操作,所以沒有碎片問題。ZGC 還能感知 NUMA 架構,提高內存的訪問速度。與傳統(tǒng)的收集算法相比,ZGC 直接在對象的引用指針上做文章,用來標識對象的狀態(tài),所以它只能用在 64 位的機器上。
現(xiàn)在在線上使用 ZGC 的還非常少。即使是用,也只能在 Linux 平臺上使用。等待它的普及,還需要一段時間。
?
小結
相對于 CMS,G1 有了更可靠的駕馭度。而且有 RSet 和 SATB 等算法的支撐,Remark 階段更加高效。
G1 最重要的概念,其實就是 Region。它采用分而治之,部分收集的思想,盡力達到我們給它設定的停頓目標。
G1 的垃圾回收過程分為三種,其中,并發(fā)標記階段,為更加復雜的 Mixed GC 階段做足了準備。
?
總結
- 上一篇: 从热门话题世界这么大我想去看看想到的:年
- 下一篇: Markdown键盘样式,文献引用