JVM系列之垃圾回收器——G1的運行原理以及調優思路

1. G1 垃圾回收器

Garbage First 簡稱 G1,是繼 CMS 垃圾回收器之後,又一款併發的垃圾回收器,在 JDK7 中被去掉 Experimental 標識,開始可以被正式使用,在 JDK9 中被 JVM 設置爲默認的垃圾回收器。G1 是垃圾收集器發展史上的一個新的里程碑,它採用分區算法,基於 Region 的內存佈局方式,對整個堆內存進行局部回收,既能回收新生代,也能回收老年代。G1 垃圾回收器的目標是在期望的停頓時間內,儘可能地提高系統的吞吐量。

2. G1 的特點

與上篇文章(JVM 系列之經典垃圾回收器(上篇))中提到的 6 款垃圾回收器相比,G1 垃圾回收器在垃圾回收過程中,不僅支持並行,還支持併發。另外 G1 在內存佈局以及實現思路上,與前面介紹的垃圾回收器具有非常大的不同之處。

2.1 region 分區

雖然 G1 仍然遵循分代收集理論,但是 G1 不再堅持固定大小、固定數量的分代區域劃分,而是將整個內存區域劃分爲若干個大小相等的獨立小區域(Region),每個 Region 都能扮演 Eden、Survivor、Old 區。新生代和老年代的內存在物理上不再是連續的,而是邏輯上處於連續。示意圖如下。

JVM系列之垃圾回收器(中篇)——G1的運行原理以及調優思路

 

G1分區示意圖

在 G1 中,新增了一個 H 區的概念,如果一個對象的大小超過了一個 Region 的 50%,那麼該對象就會被直接存放進 H 區。如果一個 Region 無法存放下對象,那麼就會採用連續的多個 Region 來存放該超大對象。

每個 Region 的大小可以通過參數 「-XX:G1HeapRegionSize」 設置,取值範圍爲 1MB~32MB,且爲 2 的整數次冪。通常情況下,G1 會將堆內存劃分爲 2048 個 Region,如果我們指定堆內存的大小爲 4G ,那麼每個 Region 的大小爲 2MB。

2.2 停頓時間

G1 的另外一大特點是可以設置一個期望的停頓時間,然後在期望的停頓時間內,對「一部分 Region」進行垃圾回收。在平時的工作中,我們經常爲 JVM 設置合理的內存大小,優化部分參數,其實就是爲了儘量減少 Minor GC 和 Full GC,從而減少系統的停頓時間,而 G1 垃圾回收器直接我們提供了最大停頓時間這個參數(「-XX:MaxGCPauseMillis」)。

那麼 G1 是如何實現在期望的停頓時間內,完成垃圾回收的呢?

實際上,在系統運行過程中,G1 會收集每個 Region 的回收耗時、垃圾佔比等各個可測量的信息,然後計算回收每個 Region 帶來的收益大小(可回收的內存+回收耗時),通過維護一個優先級列表,然後在設置的最大停頓時間內,回收那些能帶來最大收益的 Region。

雖然 G1 爲我們提供了最大停頓時間這個參數,但是我們也不能異想天開的認爲,這個參數設置得越小越好。如果設置得太小,那麼會因爲每次 GC 可以停頓的時間太少,導致每次 GC 只能回收極少的 Region,如果此時內存的分配速度大於 Region 回收的速度,那麼在系統初期,可能會因爲還有空閒內存可以支撐一段時間,但是時間一長,就會導致空閒內存越來越少,最終觸發 Full GC,從而導致系統停頓時間更長。

2.3 併發執行

在併發標記階段,G1 的垃圾回收線程和用戶線程,是併發執行的,那麼 G1 是如何保證垃圾回收線程與用戶線程互不干擾的呢?在 CMS 中,採用的是增量收集算法,而在 G1 中採用的原始快照算法(SATB)。

2.4 運行流程

如果不考慮在垃圾回收過程中,用戶線程的運行動作(如使用寫屏障來維護記憶集等操作),那麼 G1 的運行流程大致可以分爲如下四個步驟:初始標記、併發標記、最終標記、篩選回收。

  1. 「初始標記」。僅僅只是標記出 GC Roots 直接關聯的對象(此時當前 Region 中的記憶集也會被當做是 GC Roots),並且還會修改 TAMS 指針,讓下一階段用戶線程併發執行時,能夠正確的在可用的 Region 中分配新對象。這一步會造成 STW,但是由於只標記和 GC Roots 直接相連的對象,所以暫停時間很短,具體暫停多長時間,和 GC Roots 的數量有關。另外由於該階段是借用進行 Minor GC 是同步完成的,因此不會額外造成停頓。
  2. 「併發標記」。從上一步標記出的對象出發,遍歷整個對象圖,這一步耗時較長,但是由於是和用戶線程併發執行,因此不會造成 STW。
  3. 「最終標記」。由於在併發標記階段,垃圾回收線程和用戶線程併發執行,因此在這一過程中,可能會由於用戶線程改變了對象的引用關係,造成對象”消失“,因此還需要重新處理 SATB(原始快照)記錄下在併發階段有引用關係改動的對象,這一過程就是在最終標記階段完成的,會造成 STW,否則如果用戶線程還一直進行,就會不停地造成對象引用關係的改變,我們就得不停的處理 SATB 記錄。雖然會造成 STW,但畢竟 SATB 記錄的引用改變的對象不會特別多,因此耗時比並發標記階段的耗時會少很多。在這一步中,如果發現當前 Region 中的所有對象都是垃圾對象,那麼就會立即對當前 Region 進行回收。
  4. 「篩選回收」。負責更新 Region 的統計數據,根據每個 Region 的回收價值和成本進行排序,然後根據用戶期望停頓的時間內來指定回收計劃,可以選擇多個 Region 構成回收集,然後採用複製算法,將 Region 中存活的對象複製到空閒的 Region 中,從而回收 Region。

JVM系列之垃圾回收器(中篇)——G1的運行原理以及調優思路

 

G1運行示意圖

整體上看,G1 垃圾回收的 4 個階段,只有併發標記階段不會造成 STW,其他階段都會產生 STW,因此它並非純粹的追求低延時。

關於上面提到的「記憶集、對象”消失"、TAMS 指針、SATB(原始快照)」 等概念,有興趣的朋友可以自行上網查閱,內容較多,這裏就不展開說明。

2.5 優缺點

與同樣具有低延時的垃圾回收器 CMS 相比,G1 既有優點也有缺點。

首先,G1 中可以指定最大停頓時間、對內存進行 Region 分區、按照收益動態進行垃圾回收,這些特性帶來的紅利都是 CMS 所不具有的。另外,G1 垃圾回收器從局部看,採用的的是複製算法,將一個 Region 中存活的對象複製到另一個 Region 中;從整體上看,G1 回收器採用的是標記-壓縮(整理)算法。這兩種算法最終都不會帶來內存碎片,這有利於系統的長時間運行。而 CMS 則是採用的是標記-清除算法,會帶來內存碎片,當連續內存不足以分配一個對象時,會產生 Full GC。

雖然 G1 的優點很多,但是它還不足以完全替代 CMS,它也存在在很明顯的缺點。

「G1 的內存佔用相對而言,比較大」。G1 堆內存採用 Region 分區設計,每個 Region 中都存在一個記憶集,而其他傳統的垃圾回收器中,整個堆內存只需要維護一份記憶集即可,因此 G1 中記憶集所佔用的內存相比傳統的垃圾回收器而言,會大很多。「加上其他內存消耗,G1 所佔用的內存空間可能達到堆內存的 20%,甚至更多」。(這個數據參考自周志明《深入理解 Java 虛擬機》第三版)。

「G1 對系統造成的負載較高」。G1 和 CMS 都是用到了寫屏障來維護記憶集,不同的是,CMS 使用了寫後屏障來維護記憶集,而 G1 在設計上由於更復雜,不僅使用了寫前屏障還使用了寫後屏障。G1 中寫前屏障用來跟蹤併發時的指針變化,從而實現 SATB(原始快照算法),使用寫後屏障來維護記憶集中的卡表。由於 G1 對寫屏障的複雜操作比 CMS 會消耗更多的資源,因此在 CMS 中,直接使用同步操作來實現寫屏障,而在 G1 中不得不使用類似於隊列的數據結構來實現寫前屏障和寫後屏障,進行異步處理。

在重新標記階段,CMS 使用的是增量更新算法,而 G1 使用的是 SATB(原始快照)算法,原始快照搜索能夠減少在併發標記階段和最終標記階段的時間消耗,避免像 CMS 在最終標記階段停頓時間過程的缺點,但是原始快照算法會使系統的負載加重。

總的來說,G1 並不能在任何場景下取代 CMS,「G1 更適合在大內存的機器中使用,CMS 更適合在小內存機器中使用,這個內存大小的界限大概爲 6~8G」。(這個數值也是參考自周志明《深入理解 Java 虛擬機》第三版一書)。

3. G1 垃圾回收器的運行細節

G1 垃圾回收器既能回收新生代,又能回收老年代,那麼究竟在什麼情況下會觸發新生代 GC,什麼情況下觸發老年代 GC 呢?

3.1 什麼時候觸發新生代 GC

在 G1 中,Eden、Survivor、老年代的大小是在動態變化的。在初始時,新生代佔整個堆內存的 5%,可以通過參數「G1NewSizePercent」設置,默認值爲 5。

在 G1 中,雖然進行了 Region 分區,但是新生代依舊可以被分爲 Eden 區和 Survivor 區,參數 SurvivorRatio 依舊錶示 Eden/Survivor 的比值。

隨着系統的運行,Eden 區的對象越來越多,當達到 Eden 區的最大大小時,就會觸發 Minor GC。新生代的最大大小默認爲整個堆內存的 60%,可以通過參數「G1MaxNewSizePercent」控制,默認值爲 60。

G1 垃圾回收器在進行新生代的垃圾回收時,會採用複製算法來回收垃圾,不用考慮併發的場景,全程都是 STW,它會根據設置的停頓時間,儘可能的最大效率的回收新生代區域。

3.2 什麼時候進入到老年代

新生代的對象要進入老年代,需要達到以下兩個條件中的其中之一即可。

  1. 「多次躲過新生代的回收」,對象年齡達到「MaxTenuringThreshold」,該參數默認值爲 15。 在每次 Minor GC 時,新生代的對象如果存活,會被移動到 Survivor 區中,同時會將對象的年齡加 1,當對象的年齡達到 MaxTenuringThreshold 後,就被被移到老年代中。
  2. 「符合動態年齡判斷規則」。如果某次 Minor GC 過後,發現 Survivor 區中相同年齡的對象達到了 Survivor 的 50%,那麼該年齡及以上的對象,會被直接移動到老年代中。 例如某次 Minor GC 過後,Survivor 區中存在年齡分別爲 1、2、3、4 的對象,而年齡爲 3 的對象超過了 Survivor 區的 50%,那麼年齡大於等於 3 的對象,就會被全部移動到老年代中。

3.3 什麼時候觸發混合 GC

在 G1 中,「不存在單獨回收老年代的行爲,而是當要發生老年代的回收時,同時也會對新生代以及大對象進行回收,因此這個階段稱之爲混合回收(Mixed GC)」

當老年代對堆內存的佔比達到 45%時,就會觸發混合回收。可以通過參數
InitiatingHeapOccupancyPercent」
來設置當堆內存達到多少時,觸發混合 GC,該參數的默認值爲 45。

當觸發混合 GC 時,會依次執行初始標記(在 Minor GC 時完成)、併發標記、最終標記、篩選回收這四個過程。最終會根據設置的最大停頓時間,來計算對哪些 Region 區域進行回收帶來的收益最大。

實際上,在篩選回收階段,可以分多次回收 Region,具體多少次可以通過參數「G1MixedGCCountTarget」控制,默認值爲 8 次。具體什麼意思呢?

假如現在有 80 個 Region 需要被回收,因爲篩選回收階段會造成 STW,如果一下子全部回收這 80 個 Region,可能造成的停頓時間較長,因此 JVM 會分 8 次來回收這些 Region,每次先回收 10 個 Region,然後讓用戶線程執行一會,接着再讓 GC 線程回收 10 個 Region,直至回收完這 80 個 Region,這樣儘可能的降低了系統的暫停時間。

G1 垃圾回收器的回收思路是:不需要對整個堆進行回收,只需要保證垃圾回收的速度大於內存分配的速度即可。因此在每次進行 Mixed GC 時,雖然我們設置了停頓時間,但是當回收得到的空閒 Region 數量達到了整個堆內存的 5%,那麼就會停止回收。可以由參數「G1HeapWaterPercent」控制,默認值爲 5%。

另外,在混合回收的過程中,由於使用的是複製算法,因此當一個 Region 中存活的對象過多的話,複製這個 Region 所耗費的時間就會較多,因此 G1 提供了一個參數,用來控制當存活對象佔當前 Region 的比例超過多少後,就不會對該 Region 進行回收。該參數爲
G1MixedGCLiveThresholdPercent」
,默認值爲 85%。

3.4 觸發 Full GC

在進行混合回收時,使用的是複製算法,如果當發現空閒的 Region 大小無法放得下存活對象的內存大小,那麼這個時候使用複製算法就會失敗,因此此時系統就不得不暫停應用程序,進行一次 Full GC。進行 Full GC 時採用的是單線程進行標記、清理和整理內存,這個過程是非常漫長的,因此應該儘量避免 Full GC 的觸發。

4. 調優思路

介紹了這麼多 G1 相關的知識,而然實際上 G1 用起來卻十分簡單:-XX:+UseG1GC,難的是 JVM 的系統調優。G1 垃圾回收器中最重要的一個參數是:「MaxGCPauseMillis」,而要對 G1 進行調優,大概率就是結合系統的實際情況,來調整 MaxGCPauseMillis 的值。

該值設置的太小,雖然在 GC 回收時停頓時間較短,但是每次回收的 Region 也會較少,如果內存分配速度過快,就需要頻繁的進行 GC,當回收速度跟不上內存分配速度時,會造成 Full GC。

如果設置得過大,那麼雖然每次回收可以獲得的空閒 Region 較多,但是系統停頓時間會過長,也不好。因此需要結合系統的實際情況,通過相關的工具,實時查看系統的內存情況,從而決定如何調整該參數。

另外應該儘量「減少 Mixed GC 發生的次數」。觸發 Mixed GC 的條件是老年代佔用堆內存到達 45%時,因此可以適當地調大該值。不建議使用,儘量使用默認值即可。

我們可以從源頭上考慮,觸發混合 GC 是因爲老年代對象過多,而老年代的對象從哪兒來的?當 Survivor 區中的對象年齡達到閾值或者存活的對象數量太多,導致 Survivor 無法容納下,最終使對象進入到老年代。

如果 MaxGCPauseMillis 設置得過大,會導致很久才進行一次新生代回收,由於新生代的對象積攢過多,存活的對象數量也可能比較多,當 Survivor 無法存放下時,可能觸發動態年齡判斷條件,從而導致對象直接進入到老年代中,進而導致 Mixed GC。

如果 MaxGCPauseMillis 設置得過小,導致新生代回收頻繁,存活對象的年齡增長過快,從而進入到老年代,又會造成 Mixed GC。

因此想要減少 Mixed GC 發生的次數,其核心也是需要控制 MaxGCPauseMillis 參數的大小。

關於 G1 垃圾回收器,它有很多參數可以進行設置,在具體使用過程中,如何進行調優,需要結合實際情況進行設置。這裏筆者只是提供一個思路,個人認爲「MaxGCPauseMillis」參數是 G1 調優的核心,且能對哪些參數進行調優的前提是:需要明白 G1 垃圾收集器的工作原理以及這些參數對 G1 是如何影響的。

5. 總結

本文主要介紹了 G1 垃圾收集器的工作原理,以及相關特點,如 Region 分區、可控的停頓時間等,相比較另外 6 款經典的垃圾回收器,這些新的特性促使 G1 的回收效率更高,應用更加廣泛。

最後結合 G1 的工作原理,提供了一種 G1 的調優思路:結合實際情況調整 MaxGCPauseMillis 參數的值。

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