探索G1垃圾回收器

王子發表於2020-11-04

 

前言

最近王子因為個人原因有些忙碌,導致文章更新比較慢,希望大家理解,之後也會持續和小夥伴們一起共同分享技術乾貨。

上篇JVM的文章中我們對ParNew和CMS垃圾回收器已經有了一個比較透徹的認識,感興趣的小夥伴可以去回看一下探索ParNew和CMS垃圾回收器

今天我們繼續探索垃圾回收器G1的原理,讓我們開始吧!

 

G1的記憶體模型

G1是從jdk7開始出現的,在jdk9中被設為預設垃圾收集器,目標就是徹底替換掉CMS,那麼為什麼它可以替換掉CMS呢?

首先我們就來看看它的記憶體模型吧。

其實G1是可以同時回收年輕代和老年代的,他最大的特點就是把jvm堆記憶體拆分為了多個大小相等的Region,那麼還存在年輕代和老年代嗎?

答案是肯定的,不同的是新生代可能包含了某些Region,老年代也可能包含了某些Region,如下圖:

 

 

到底有多少Region?每個Region有多大呢?

其實這個預設情況下是自動計算的,假如我們給定整個堆記憶體大小為4096M,然後使用“-XX:+UseG1GC”指定垃圾回收器為G1,此時會自動用堆記憶體大小除以2048,因為JVM最多可以有2048個Region,然後Region的大小必須是2的倍數。

堆記憶體為4096M,就會分配給每個Region 2M的記憶體空間。我們使用G1預設的計算方式就可以了。

當然也可以通過引數“-XX:G1HeapRegionSize”來指定Region的大小。

新生代和老年代的預設比例是多少呢?

我們知道使用ParNew和CMS垃圾回收器時,新生代和老年代的預設比例是1:2,而使用G1後,預設新生代對堆記憶體的初始佔比是5%,這個可以通過“-XX:G1NewSizePercent”來設定初始佔比,一般不需要設定。

細心的小夥伴會發現,這裡說的佔比是初始佔比,因為系統執行的時候,JVM其實會不停的給新生代增加更多的Region,但是最多新生代的佔比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”來設定。

而一旦發生了垃圾回收,新生代的Region數量還會減少,所以其實新生代和老年代的佔比不是一成不變的,而是動態改變的。

新生代還有eden和survivor嗎?

答案是肯定的,新生代還是有eden和survivor的,只不過記憶體佔用會隨著Region的增多而增大。

 

G1的停頓時間控制

除了記憶體的變化,G1還有一個最大的變化,就是可以讓我們設定一個垃圾回收的預期停頓時間,也就是說我們可以指定G1垃圾回收導致“Stop the World”的最長時間。

我們知道JVM一大痛點就是"Stop the World",儘量減少它的時間就可以做到JVM的優化。

引入G1後,我們可以自己去設定這個停頓的最長時間了,相當於直接控制了垃圾回收的效能。

G1要做到這一點就要去追蹤每個Region的回收價值,那什麼是回收價值呢?大家看下圖:

 

比如兩個Region中,其中一個有10M的垃圾物件,垃圾回收需要耗時1s,另一個有20M的垃圾物件,垃圾回收耗時200ms。

然後G1進行垃圾回收的時候,發現最近1小時垃圾回收已經導致了幾百毫秒的系統停頓了,所以會選擇回收價值高的Region進行回收,200ms的時間就能回收掉20M的垃圾物件,回收價值相對較高,所以會選擇這個Region進行回收

G1控制停頓時間的思路,簡單來講就是,它會通過跟蹤Region的回收價值,儘可能的保證系統停頓時間在你設定的停頓時間範圍內。

 

G1的垃圾回收詳解

上文我們瞭解到新生代還是有eden和survivor的,那麼隨著新生代佔據堆記憶體大小的60%的時候,這個時候就會觸發新生代的GC,G1也會使用之前我們說過的複製演算法進行垃圾回收,進入一個“Stop the World”狀態。

但是這個過程與之前的Minor GC其實是有差別的,首先回收的物件變成了帶有垃圾物件的Region,然後回收的同時會根據設定的停頓時間進行價值回收,如上文所述。

 

什麼時候進入老年代呢?

這個可以說和之前是一模一樣的,簡單介紹如下:

新生代躲過多次垃圾回收後會進入老年代;

GC後存活物件超過Survivor區的50%,那麼會觸發動態年齡判定規則,符合規則的進入老年代。

具體細節不在說明,可以參考王子之前的文章秒懂JVM的垃圾回收機制,有詳細解釋。

需要注意的是,G1的大物件不是存到老年代中的,而是提供了專門的Region來存放大物件。

在G1中,大物件的判斷規則就是這個物件超過了一個Region大小的50%,比如Region是2M的,那如果你的物件超過了1M,就會被認定為大物件,做特殊處理。

而且如果這個大物件過大,可以橫跨多個Region進行儲存,如下圖:

 

 

老年代具體又是怎麼進行垃圾回收的呢

這個過程說起來可能稍微複雜了一點,但是它和CMS的垃圾回收過程其實是類似的。關於CMS的垃圾回收的幾個階段可以回顧王子的上篇文章探索ParNew和CMS垃圾回收器

首先我們要弄明白,什麼時候會觸發新生代和老年代的混合垃圾回收?

G1有一個引數“-XX:InitiatingHeapOccupancyPercent”,預設值為45%。

什麼意思呢?就是說當老年代佔據了堆記憶體的45%的Regionf的時候,就會觸發混合垃圾回收。

 

具體流程是什麼樣的呢?

首先會觸發一次初始標記操作,這個過程是要“Stop the World”的,對應的就是CMS的初始標記階段,細節不再說明。

接著會進入併發標記階段,這個階段同樣對應CMS的併發標記階段,不再說明。

接著會進入最終標記階段,這個階段其實和CMS的重新標記階段也基本一致。

最後就是混合回收階段,這個階段和CMS的併發清理階段就不太一樣了,這個階段會計算每個Region中的存活物件數量,存活數量佔比,還有執行垃圾回收的耗時等問題。

接著會進入“Stop the World”階段,然後全力以赴進行垃圾回收,並儘量保證停止時間不超過我們設定好的時間,所以可能只會回收掉之前標記好的一部分垃圾物件。

為什麼要叫做混合回收呢,因為它不僅僅回收的是老年代,新生代和大物件的Region也會同時進行回收,而具體回收哪些Region就要視情況而定了,根據價值回收價值G1會自己做出選擇。

而混合回收是可以進行多次的,比如先停止系統,混合回收掉一部分Region,再停止系統,再執行一次混合回收。

有引數可以控制這個數量,“-XX:G1MixedGCCountTarget”引數,就是在一次混合回收的過程中,最後一個階段執行幾次,預設是8次。

 

為什麼要這樣反覆多次的回收呢?

因為這樣每次回收停止系統的時間都很短,在回收的間隙系統是可以正常執行的。

 

還有個引數“-XX:G1HeapWastePercent”,預設值是5%。

它的意思是,混合回收的時候,都是基於複製演算法進行的,把Region存活的物件放入其他Region,然後清除掉本來的Region。那麼當空閒的Region數量達到堆記憶體的5%,就會立即停止混合回收。

而通過這種複製演算法回收,也不會出現像CMS標記清理演算法導致的記憶體碎片問題。

 

還有個引數“-XX:G1MixedGCLiveThresholdPercent”,預設值是85%,意思就是回收Region的時候,存活的物件必須少於85%才可以被回收掉。否則存活物件太多,複製的時候成本是很高的。

 

如果回收失敗怎麼辦?

如果在複製的時候發現沒有空閒的Region可以承載存活的物件,那麼會觸發失敗,立馬停止系統程式,採用單執行緒進行標記、清理和壓縮整理,空閒出一批Region,這個過程是極慢的。

 

總結

本文我們對G1的記憶體機制和垃圾回收的演算法做了一個比較清晰的解釋。

閱讀完本文,相信小夥伴們自己可以總結出G1和CMS究竟有什麼不一樣了吧。

歡迎小夥伴們留言區討論G1和CMS的區別,王子會第一時間回覆。

那我們下篇文章再見。

 

 

往期文章推薦:

大白話談JVM的類載入機制

JVM記憶體模型不再是祕密

輕鬆理解JVM的分代模型

秒懂JVM的垃圾回收機制

探索ParNew和CMS垃圾回收器

 

相關文章