探索G1垃圾回收器

 

前言

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

上篇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垃圾回收器

 

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