G1 中提供了 Young GC、Mixed GC 兩種垃圾回收模式,這兩種垃圾回收模式,都是 Stop The World(STW) 的。
G1 沒有 fullGC 概念,需要 fullGC 時,呼叫 serialOldGC 進行全堆掃描(包括 eden、survivor、o、perm)。
一、G1 堆記憶體結構
堆記憶體會被切分成為很多個固定大小區域(Region),每個是連續範圍的虛擬記憶體。
1、Region
堆記憶體中一個區域 (Region) 的大小,可以透過 -XX:G1HeapRegionSize 引數指定,大小區間最小 1M 、最大 32M ,總之是 2 的冪次方。
預設是將堆記憶體按照 2048 份均分。
每個 Region 被標記了 E、S、O 和 H,這些區域在邏輯上被對映為 Eden,Survivor 和老年代。
存活的物件從一個區域轉移(即複製或移動)到另一個區域。區域被設計為並行收集垃圾,可能會暫停所有應用執行緒。如上圖所示,區域可以分配到 Eden,survivor 和老年代。
此外,還有第四種型別,被稱為巨型區域(Humongous Region)。
Humongous 區域主要是為儲存超過 50% 標準 region 大小的物件設計,它用來專門存放巨型物件。如果一個 H 區裝不下一個巨型物件,那麼 G1 會尋找連續的 H 分割槽來儲存。為了能找到連續的 H 區,有時候不得不啟動 Full GC 。
2、小物件
G1預設啟用了UseTLAB最佳化,建立物件(小物件)時,優先從TLAB中分配記憶體,如果分配失敗,說明當前TLAB的剩餘空間不滿足分配需求,則呼叫allocate_new_tlab方法重新申請一塊TLAB空間,之前都是從eden區分配,G1需要從eden region中分配,不過也有可能TLAB的剩餘空間還比較大,JVM不想就這麼浪費掉這些記憶體,就會從eden region中分配記憶體。
3、大物件
要特別注意的是,巨型物件(Humongous Object),即大小超過 3/4 的 Region 大小的物件會作特殊處理,分配到由一個或多個連續 Region 構成的區域。巨型物件會引起其他一些問題。
二、停頓預測模型
Pause Prediction Model 即停頓預測模型。
它在G1中的作用是: >G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
G1 GC是一個響應時間優先的GC演算法,它與CMS最大的不同是,使用者可以設定整個GC過程的期望停頓時間,引數-XX:MaxGCPauseMillis指定一個G1收集過程目標停頓時間,預設值200ms,不過它不是硬性條件,只是期望值。
G1根據這個模型統計計算出來的歷史資料來預測本次收集需要選擇的Region數量,從而儘量滿足使用者設定的目標停頓時間。
停頓預測模型是以衰減標準偏差為理論基礎實現的:
// share/vm/gc_implementation/g1/g1CollectorPolicy.hpp double get_new_prediction(TruncatedSeq* seq) { return MAX2(seq->davg() + sigma() * seq->dsd(), seq->davg() * confidence_factor(seq->num())); }
在這個預測計算公式中:davg表示衰減均值,sigma()返回一個係數,表示信賴度,dsd表示衰減標準偏差,confidence_factor表示可信度相關係數。
而方法的引數TruncateSeq,顧名思義,是一個截斷的序列,它只跟蹤了序列中的最新的n個元素。
三、YoungGC 年輕代收集
在分配一般物件(非巨型物件)時,當所有 eden region 使用達到最大閥值、並且無法申請足夠記憶體時,會觸發一次 YoungGC 。
每次 younggc 會回收所有Eden 、以及 Survivor 區,並且將存活物件複製到 Old 區以及另一部分的 Survivor 區。
第一階段:掃描根
跟 CMS 類似,Stop the world,掃描 GC Roots 物件;
第二階段:處理 Dirty card,更新 RSet
處理 dirty card queue 中的 card,更新 RSet。此階段完成後,RSet 可以準確的反映老年代對所在的記憶體分段中物件的引用。
第三階段:掃描 RSet
掃描 RSet 中所有 old 區,對掃描到的 young 區或者 survivor 區的引用;
第四階段:複製掃描出的存活的物件到 survivor2/old 區
Eden 區記憶體段中存活的物件會被複制到 Survivor 區中空的記憶體分段,Survivor 區記憶體段中存活的物件如果年齡未達閾值,年齡會加1,達到閥值會被會被複制到 old 區中空的記憶體分段。如果 Survivor 空間不夠,Eden 空間的部分資料會直接晉升到老年代空間。
第五階段:處理引用佇列、軟引用、弱引用、虛引用
處理 Soft,Weak,Phantom,Final,JNI Weak 等引用。
最終 Eden 空間的資料為空,GC 停止工作,而目標記憶體中的物件都是連續儲存的,沒有碎片,所以複製過程可以達到記憶體整理的效果,減少碎片。
四、Mixed GC 混合GC
多次 Young GC 之後,當越來越多的物件晉升到老年代 old region,Old Regions 慢慢累積,直到到達閾值(InitiatingHeapOccupancyPercent,簡稱 IHOP),我們不得不對 Old Regions 做收集。這個閾值在 G1 中是根據使用者設定的 GC 停頓時間動態調整的,也可以人為干預。
對 Old Regions 的收集會同時涉及若干個 Young 和 Old Regions,因此被稱為 Mixed GC。
Mixed GC 很多地方都和 Young GC 類似,不同之處是:它還會選擇若干最有潛力的 Old Regions(收集垃圾的效率最高的 Regions),這些選出來要被 Evacuate 的 Region 稱為本次的 Collection Set (CSet)。
這裡需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些 old region 進行收集,從而可以對垃圾回收的耗時時間進行控制。
結合Region 的設計,只要把每次的 Collection Set 規模控制在一定範圍,就能把每次收集的停頓時間軟性地控制在 MaxGCPauseMillis 以內。起初這個控制可能不太精準,隨著 JVM 的執行估算會越來越準確。
那來不及收集的那些 Region 呢?多來幾次就可以了。所以你在 GC 日誌中會看到 continue mixed GCs 的字樣,代表分批進行的各次收集。這個過程會多次重複,直到垃圾的百分比降到 G1HeapWastePercent 以內,或者到達 G1MixedGCCountTarget 上限。
1、STAB和TAMS
在 Evacuation 之前,我們要透過併發標記來確定哪些物件是垃圾、哪些還活著。G1 中的 Concurrent Marking 是以 Region 為單位的,為了保證結果的正確性,這裡用到了 Snapshot-at-the-beginning(SATB)演算法。
SATB 演算法顧名思義是對 Marking 開始時的一個(邏輯上的)Snapshot 進行標記。為什麼要用 Snapshot 呢?下面就是一個直接標記導致問題的例子:物件 X 由於沒有被標記到而被標記為垃圾,導致 B 引用失效。
如果只是對現場情況做標記,可能會漏掉某些物件。SATB 演算法為了解決這一問題,在修改引用 X.f = B 之前插入了一個 Write Barrier,記錄下被覆寫之前的引用地址。這些地址最終也會被 Marking 執行緒處理,從而確保了所有在 Marking 開始時的引用一定會被標記到。
這個 Write Barrier 虛擬碼如下:
1 2 3 |
t = the previous referenced address // 記錄原本的引用地址 if (t has been marked && t != NULL) // 如果地址 t 還沒來的及標記,且 t 不為 NULL satb_enqueue(t) // 放到 SATB 的待處理佇列中,之後會去掃描這個引用 |
透過以上措施,SATB 確保 Marking 開始時存活的物件一定會被標記到。
2、Concurrent Marking
G1標記的過程和 CMS 中是類似的,可以看作一個最佳化版的 DFS:記當前已經標記到的 offset 為 cur,隨著標記的進行 cur 不斷向後推進。每當訪問到地址 < cur 的物件,就對它做深度掃描,遞迴標記所有應用;反之,對於地址 > cur 的物件,只標記不掃描,等到 cur 推進到那邊的時候再去做掃描。
上圖中,假設當前 cur 指向物件 c,c有兩個引用:a 和 e,其中 a 的地址小於 cur,因而做了掃描;而 e 則僅僅是標記。掃描 a 的過程中又發現了物件 b,b 同樣被標記並繼續掃描。但是 b 引用的 d 在 cur 之後,所以 d 僅僅是被標記,不再繼續掃描。
最後一個問題是:如何處理 Concurrent Marking 中新產生的物件?因為 SATB 演算法只保證能標記到開始時 snapshot 的物件,對於新出現的那些物件,我們可以簡單地認為它們全都是存活的,畢竟數量不是很多。
2、回收過程
G1垃圾回收週期如下圖所示:
G1的Mixed GC回收過程可以分為標記階段、清理階段和複製階段。
- 初始標記階段:初始標記階段是指從GC Roots出發標記全部直接子節點的過程,該階段是STW的。由於GC Roots數量不多,通常該階段耗時非常短。
- 併發標記階段:併發標記階段是指從GC Roots開始對堆中物件進行可達性分析,找出存活物件。該階段是併發的,即應用執行緒和GC執行緒可以同時活動。併發標記耗時相對長很多,但因為不是STW,所以我們不太關心該階段耗時的長短。
- 再標記階段:重新標記那些在併發標記階段發生變化的物件。該階段是STW的。
- 清理階段清點出有存活物件的分割槽和沒有存活物件的分割槽,該階段不會清理垃圾物件,也不會執行存活物件的複製。該階段是STW的。
- 複製演算法中的轉移階段需要分配新記憶體和複製物件的成員變數。轉移階段是STW的,其中記憶體分配通常耗時非常短,但物件成員變數的複製耗時有可能較長,這是因為複製耗時與存活物件數量與物件複雜度成正比。物件越複雜,複製耗時越長。
四個STW過程中,初始標記因為只標記GC Roots,耗時較短。
再標記因為物件數少,耗時也較短。清理階段因為記憶體分割槽數量少,耗時也較短。
轉移階段要處理所有存活的物件,耗時會較長。
因此,G1停頓時間的瓶頸主要是標記-複製中的轉移階段STW。
為什麼轉移階段不能和標記階段一樣併發執行呢?主要是G1未能解決轉移過程中準確定位物件地址的問題。
五、Serial Old GC
如果mixed GC實在無法跟上程式分配記憶體的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GC(full GC)來收集整個GC heap。所以我們可以知道,G1是不提供full GC的。
Serial Old是Serial收集器的老年代版本,是一個單執行緒收集器,使用標記-整理演算法。
1、Serial Old收集
Serial收集器過程如下:
優點:演算法簡單,記憶體佔用少,CPU不用切換程式,導致上下文切換時間短,總體效率高
缺點:GC階段卡頓
2、G1產生FGC如何解決
- 擴充套件記憶體
- 提高CPU效能(回收的快,業務邏輯產生物件的速度固定,垃圾回收越快,記憶體空間越大)
- 降低MixedGC觸發的閾值,讓MixedGC提早發生(預設是45%)
六、對比CMS
1、G1 相比較 CMS的改進
- 演算法: G1 基於標記--整理演算法, 不會產生空間碎片,在分配大物件時,不會因無法得到連續的空間,而提前觸發一次 FULL GC 。
- 停頓時間可控: G1可以透過設定預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象。
- 並行與併發:G1 能更充分的利用 CPU 多核環境下的硬體優勢,來縮短 stop the world 的停頓時間。
2、CMS 和 G1 的區別
- CMS 中,堆被分為 PermGen,YoungGen,OldGen ;而 YoungGen 又分了兩個 survivo 區域。在 G1 中,堆被平均分成幾個區域 (region) ,在每個區域中,雖然也保留了新老代的概念,但是收集器是以整個區域為單位收集的。
- G1 在回收記憶體後,會立即同時做合併空閒記憶體的工作;而 CMS ,則預設是在 STW(stop the world)的時候做。
- G1 會在 Young GC 中使用;而 CMS 只能在 O 區使用。
參考資料:
https://ericfu.me/g1-garbage-collector/
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html