前言
最近王子因為個人原因有些忙碌,導致文章更新比較慢,希望大家理解,之后也會持續和小伙伴們一起共同分享技術干貨。
上篇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的區別,王子會第一時間回復。
那我們下篇文章再見。
往期文章推薦: