垃圾收集器G1

G1收集器(-XX:+UseG1GC)

G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵.

垃圾收集器G1

垃圾收集器G1

G1將Java堆劃分為多個大小相等的獨立區域(

Region

),JVM目標是

不超過2048個Region

(JVM原始碼裡TARGET_REGION_NUMBER 定義),實際可以超過該值,但是不推薦。

一般Region大小等於堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當然也可以用引數“-XX:G1HeapRegionSize”手動指定Region大小,但是推薦預設的計算方式。

G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續)Region的集合。

預設年輕代對堆記憶體的佔比是5%,如果堆大小為4096M,那麼年輕代佔據200MB左右的記憶體,對應大概是100個Region,可以透過“-XX:G1NewSizePercent”設定新生代初始佔比,在系統執行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的佔比不會超過60%,可以透過“-XX:G1MaxNewSizePercent”調整。年輕代中的Eden和Survivor對應的region也跟之前一樣,預設8:1:1,假設年輕代現在有1000個region,eden區對應800個,s0對應100個,s1對應100個。

一個Region可能之前是年輕代,如果Region進行了垃圾回收,之後可能又會變成老年代,也就是說Region的區域功能可能會動態變化。

G1垃圾收集器對於物件什麼時候會轉移到老年代跟之前講過的原則一樣,

唯一不同的是對大物件的處理

,G1有專門分配大物件的Region叫

Humongous區

,而不是讓大物件直接進入老年代的Region中。在G1中,大物件的判定規則就是一個大物件超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,只要一個大物件超過了1M,就會被放入Humongous中,而且一個大物件如果太大,可能會橫跨多個Region來存放。

Humongous區專門存放短期巨型物件,不用直接進老年代,可以節約老年代的空間,避免因為老年代空間不夠的GC開銷。

Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區一併回收。

G1收集器一次GC(主要值Mixed GC)的運作過程大致分為以下幾個步驟:

初始標記

(initial mark,STW):暫停所有的其他執行緒,並記錄下gc roots直接能引用的物件,

速度很快

併發標記

(Concurrent Marking):同CMS的併發標記

最終標記

(Remark,STW):同CMS的重新標記

篩選回收

(Cleanup,STW):篩選回收階段首先對各個Region的

回收價值和成本進行排序

根據使用者所期望的GC停頓STW時間(可以用JVM引數 -XX:MaxGCPauseMillis指定)來制定回收計劃

,比如說老年代此時有1000個Region都滿了,但是因為根據預期停頓時間,本次垃圾回收可能只能停頓200毫秒,那麼透過之前回收成本計算得知,可能回收其中800個Region剛好需要200ms,那麼就只會回收800個Region(

Collection Set

,要回收的集合),儘量把GC導致的停頓時間控制在我們指定的範圍內。這個階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。不管是年輕代或是老年代,

回收演算法主要用的是複製演算法

將一個region中的存活物件複製到另一個region中,這種不會像CMS那樣回收完因為有很多記憶體碎片還需要整理一次,G1採用複製演算法回收幾乎不會有太多記憶體碎片

。(注意:CMS回收階段是跟使用者執行緒一起併發執行的,G1因為內部實現太複雜暫時沒實現併發回收,不過到了ZGC,Shenandoah就實現了併發收集,Shenandoah可以看成是G1的升級版本)

垃圾收集器G1

G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1當然會優先選擇後面這個Region回收

。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限時間內可以儘可能高的收集效率。

被視為JDK1。7以上版本Java虛擬機器的一個重要進化特徵。它具備以下特點:

並行與併發

:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java執行緒來執行GC動作,G1收集器仍然可以透過併發的方式讓java程式繼續執行。

分代收集

:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。

空間整合

:與CMS的“標記——清理”演算法不同,G1從整體來看是基於“

標記整理

”演算法實現的收集器;從區域性上來看是基於“複製”演算法實現的。

可預測的停頓

:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立

可預測的停頓時間模型

,能讓使用者明確指定在一個長度為M毫秒的時間片段(透過引數“

-XX:MaxGCPauseMillis

”指定)內完成垃圾收集。

毫無疑問, 可以由使用者指定期望的停頓時間是G1收集器很強大的一個功能, 設定不同的期望停頓時間, 可使得G1在不同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這裡設定的“期望值”必須是符合實際的, 不能異想天開, 畢竟G1是要凍結使用者執行緒來複制物件的, 這個停頓時

間再怎麼低也得有個限度。 它預設的停頓目標為兩百毫秒, 一般來說, 回收階段佔到幾十到一百甚至接近兩百毫秒都很正常, 但如果我們把停頓時間調得非常低, 譬如設定為二十毫秒, 很可能出現的結果就是由於停頓目標時間太短, 導致每次選出來的回收集只佔堆記憶體很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 導致垃圾慢慢堆積。 很可能一開始收集器還能從空閒的堆記憶體中獲得一些喘息的時間, 但應用執行時間一長就不行了, 最終佔滿堆引發Full GC反而降低效能, 所以通常把期望停頓時間設定為一兩百毫秒或者兩三百毫秒會是比較合理的。

G1垃圾收集分類

YoungGC

YoungGC並不是說現有的Eden區放滿了就會馬上觸發,G1會計算下現在Eden區回收大概要多久時間,如果回收時間遠遠小於引數 -XX:MaxGCPauseMills 設定的值,那麼增加年輕代的region,繼續給新物件存放,不會馬上做Young GC,直到下一次Eden區放滿,G1計算回收時間接近引數 -XX:MaxGCPauseMills 設定的值,那麼就會觸發Young GC

MixedGC

不是FullGC,老年代的堆佔有率達到引數(

-XX:InitiatingHeapOccupancyPercent

)設定的值則觸發,回收所有的Young和部分Old(根據期望的GC停頓時間確定old區垃圾收集的優先順序)以及

大物件區

,正常情況G1的垃圾收集是先做MixedGC,主要使用複製演算法,需要把各個region中存活的物件複製到別的region裡去,複製過程中如果發現

沒有足夠的空region

能夠承載複製物件就會觸發一次Full GC

Full GC

停止系統程式,然後採用單執行緒進行標記、清理和壓縮整理,好空閒出來一批Region來供下一次MixedGC使用,這個過程是非常耗時的。(Shenandoah最佳化成多執行緒收集了)

G1收集器引數設定

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的執行緒數量

-XX:G1HeapRegionSize:指定分割槽大小(1MB~32MB,且必須是2的N次冪),預設將整堆劃分為2048個分割槽

-XX:MaxGCPauseMillis:目標暫停時間(預設200ms)

-XX:G1NewSizePercent:新生代記憶體初始空間(預設整堆5%,值配置整數,預設就是百分比)

-XX:G1MaxNewSizePercent:新生代記憶體最大空間

-XX:TargetSurvivorRatio:Survivor區的填充容量(預設50%),Survivor區域裡的一批物件(年齡1+年齡2+年齡n的多個年齡物件)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的物件都放入老年代

-XX:MaxTenuringThreshold:最大年齡閾值(預設15)

-XX:InitiatingHeapOccupancyPercent:老年代佔用空間達到整堆記憶體閾值(預設45%),則執行新生代和老年代的混合收集(

MixedGC

),比如我們之前說的堆預設有2048個region,如果有接近1000個region都是老年代的region,則可能就要觸發MixedGC了

-XX:G1MixedGCLiveThresholdPercent(預設85%) region中的存活物件低於這個值時才會回收該region,如果超過這個值,存活物件過多,回收的的意義不大。

-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(預設8次),在最後一個篩選回收階段可以回收一會,然後暫停回收,恢復系統執行,一會再開始回收,這樣可以讓系統不至於單次停頓時間過長。

-XX:G1HeapWastePercent(預設5%): gc過程中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基於複製演算法進行的,都是把要回收的Region裡的存活物件放入其他Region,然後這個Region中的垃圾物件全部清理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閒出來的Region數量達到了堆記憶體的5%,此時就會立即停止混合回收,意味著本次混合回收就結束了。

G1垃圾收集器最佳化建議

假設引數 -XX:MaxGCPauseMills 設定的值很大,導致系統執行很久,年輕代可能都佔用了堆記憶體的60%了,此時才觸發年輕代gc。

那麼存活下來的物件可能就會很多,此時就會導致Survivor區域放不下那麼多的物件,就會進入老年代中。

或者是你年輕代gc過後,存活下來的物件過多,導致進入Survivor區域後觸發了動態年齡判定規則,達到了Survivor區域的50%,也會快速導致一些物件進入老年代中。

所以這裡核心還是在於調節 -XX:MaxGCPauseMills 這個引數的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc過後的存活物件有多少,避免存活物件太多快速進入老年代,頻繁觸發mixed gc。

什麼場景適合使用G1

50%以上的堆被存活物件佔用

物件分配和晉升的速度變化非常大

垃圾回收時間特別長,超過1秒

8GB以上的堆記憶體(建議值)

停頓時間是500ms以內

每秒幾十萬併發的系統如何最佳化JVM

Kafka類似的支撐高併發訊息系統大家肯定不陌生,對於kafka來說,每秒處理幾萬甚至幾十萬訊息時很正常的,一般來說部署kafka需要用大記憶體機器(比如64G),也就是說可以給年輕代分配個三四十G的記憶體用來支撐高併發處理,這裡就涉及到一個問題了,我們以前常說的對於eden區的young gc是很快的,這種情況下它的執行還會很快嗎?很顯然,不可能,因為記憶體太大,處理還是要花不少時間的,假設三四十G記憶體回收可能最快也要幾秒鐘,按kafka這個併發量放滿三四十G的eden區可能也就一兩分鐘吧,那麼意味著整個系統每執行一兩分鐘就會因為young gc卡頓幾秒鐘沒法處理新訊息,顯然是不行的。那麼對於這種情況如何優化了,我們可以使用G1收集器,設定 -XX:MaxGCPauseMills 為50ms,假設50ms能夠回收三到四個G記憶體,然後50ms的卡頓其實完全能夠接受,使用者幾乎無感知,那麼整個系統就可以在卡頓幾乎無感知的情況下一邊處理業務一邊收集垃圾。

G1天生就適合這種大記憶體機器的JVM執行,可以比較完美的解決大記憶體垃圾回收時間過長的問題。