JVM - G1初探
文章目錄
- GC概述
- G1的內存分區(qū)
- G1回收垃圾的幾個階段
- 初始標記 (initial mark,STW)
- 并發(fā)標記(Concurrent Marking)
- 最終標記(Remark,STW)
- 篩選回收(Cleanup,STW)
- G1的特征
- G1垃圾收集分類
- YoungGC
- MixedGC
- Full GC
- G1收集器參數(shù)
- 適用場景
- G1優(yōu)化建議
- 優(yōu)化小Demo
- 參考
GC概述
G1主要針對配備多顆處理器及大容量內存的機器. 盡量滿足GC停頓時間要求的同時也具備高吞吐量。
我們知道Serial 、Serial Old 、 Parallel 、 Parallel Old 、 ParNew 、CMS這些垃圾收集器都是基于分代收集理論,即將內存區(qū)域按照存儲對象的不同分為年輕帶、老年代和元空間。如下
G1 和 ZGC 逐漸淡化了這種分代的概念,G1的堆內存分配如下
堆被劃分為一組大小相等的堆區(qū)域,每個堆區(qū)域都有一個連續(xù)的虛擬內存范圍。 這在內存使用方面提供了更大的靈活性。
G1的內存分區(qū)
G1將Java堆內存劃分為多個大小相等的獨立區(qū)域(Region),JVM最多可以有2048個Region。
一般一個Region的大小 = 堆內存/2048 .
舉個例子 4096的堆內存,這每個Region的大小為 4096/2048 = 2M .
設置堆內存的時候,適用G1, 務必為2048的倍數(shù)。
可以通過參數(shù)-XX:G1HeapRegionSize調整每個Region的大小,但不建議修改。
G1在淡化分代理論上還不是很徹底, G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們是(可以不連續(xù))Region的集合。
G1中默認年輕代對堆內存的占比是5% , 可以通過-XX:G1NewSizePercent設置新生代初始占比
在系統(tǒng)運行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過-XX:G1MaxNewSizePercent調整
年輕代中的Eden和Survivor對應的region也跟之前一樣,默認8:1:1. 意思就是假設年輕代現(xiàn)在有100個region,eden區(qū)對應80個,s0對應10個,s1對應10個。
一個Region可能之前是年輕代,如果Region進行了垃圾回收,之后可能又會變成老年代,也就是說Region的區(qū)域功能可能會動態(tài)變化。
G1中 對象什么時候會轉移到老年代 ?
原則 跟之前講過的一樣,唯一不同的是對大對象的處理
G1有專門分配大對象的Region叫Humongous區(qū),而不是讓大對象直接進入老年代的Region中。
在G1中,大對象的判定規(guī)則就是一個大對象超過了一個Region大小的50% 。
舉個例子比如按照上面算的,每個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。Humongous區(qū)專門存放短期巨型對象,不用直接進老年代,可以節(jié)約老年代的空間,避免因為老年代空間不夠的GC開銷。
Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區(qū)一并回收。
G1回收垃圾的幾個階段
初始標記 (initial mark,STW)
STW :暫停所有的其他線程,并記錄下gc roots直接能引用的對象,速度很快
并發(fā)標記(Concurrent Marking)
同CMS的并發(fā)標記
最終標記(Remark,STW)
同CMS的重新標記
篩選回收(Cleanup,STW)
首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓STW時間(可以用JVM參數(shù) -XX:MaxGCPauseMillis指定)來制定回收計劃 。
舉個例子 老年代此時有1000個Region都滿了,但是因為根據(jù)預期停頓時間,本次垃圾回收可能只能停頓200ms,那么通過之前回收成本計算得知,可能回收其中800個Region剛好需要200ms,那么就只會回收800個Region(Collection Set,要回收的集合),盡量把GC導致的停頓時間控制在我們指定的范圍內。
這個階段 G1因為內部實現(xiàn)太復雜暫時沒實現(xiàn)并發(fā)回收。到了ZGC,Shenandoah就實現(xiàn)了并發(fā)收集,Shenandoah可以看成是G1的升級版本。
不管是年輕代或是老年代,回收算法主要用的是復制算法,將一個region中的存活對象復制到另一個region中, G1采用復制算法回收幾乎不會有太多內存碎片。
G1收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來) 。
比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1當然會優(yōu)先選擇后面這個Region回收。
這種使用Region劃分內存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限時間內可以盡可能高的收集效率。
G1的特征
-
并行與并發(fā):G1能充分利用CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程來執(zhí)行GC動作,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)執(zhí)行。
-
分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
-
空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基于“標記-整理”算法實現(xiàn)的收集器;從局部上來看是基于“復制”算法實現(xiàn)的。
-
可預測的停頓:這是G1相對于CMS的另一個大優(yōu)勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段(通過參數(shù) -XX:MaxGCPauseMillis 指定)內完成垃圾收集。
G1默認的停頓目標為兩百毫秒 .
一般來說, 回收階段占到幾十到一百甚至接近兩百毫秒都很正常, 但如果我們把停頓時間調得非常低, 譬如設置為二十毫秒, 很可能出現(xiàn)的結果就是由于停頓目標時間太短, 導致每次選出來的回收集只占堆內存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 導致垃圾慢慢堆積。
很可能一開始收集器還能從空閑的堆內存中獲得一些喘息的時間, 但應用運行時間一長就不行了, 最終占滿堆引發(fā)Full GC反而降低性能, 所以通常把期望停頓時間設置為一兩百毫秒或者兩三百毫秒會是比較合理的。
G1垃圾收集分類
YoungGC
YoungGC并不是說現(xiàn)有的Eden區(qū)放滿了就會馬上觸發(fā),G1會計算下現(xiàn)在Eden區(qū)回收大概要多久時間,如果回收時間遠遠小于參數(shù) -XX:MaxGCPauseMills 設定的值,那么增加年輕代的region,繼續(xù)給新對象存放,不會馬上做Young GC,直到下一次Eden區(qū)放滿,G1計算回收時間接近參數(shù) -XX:MaxGCPauseMills 設定的值,那么就會觸發(fā)Young GC
MixedGC
不是FullGC,老年代的堆占有率達到參數(shù)(-XX:InitiatingHeapOccupancyPercent)設定的值則觸發(fā),回收所有的Young和部分Old(根據(jù)期望的GC停頓時間確定old區(qū)垃圾收集的優(yōu)先順序)以及大對象區(qū),正常情況G1的垃圾收集是先做MixedGC,主要使用復制算法,需要把各個region中存活的對象拷貝到別的region里去,拷貝過程中如果發(fā)現(xiàn)沒有足夠的空region能夠承載拷貝對象就會觸發(fā)一次Full GC
Full GC
停止系統(tǒng)程序,然后采用單線程進行標記、清理和壓縮整理,好空閑出來一批Region來供下一次MixedGC使用,這個過程是非常耗時的。(Shenandoah優(yōu)化成多線程收集了)
G1收集器參數(shù)
-
-XX:+UseG1GC:使用G1收集器
-
-XX:ParallelGCThreads:指定GC工作的線程數(shù)量
-
-XX:G1HeapRegionSize:指定分區(qū)大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分為2048個分區(qū)
-
-XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
-
-XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%,值配置整數(shù),默認就是百分比)
-
-XX:G1MaxNewSizePercent:新生代內存最大空間
-
-XX:TargetSurvivorRatio:Survivor區(qū)的填充容量(默認50%),Survivor區(qū)域里的一批對象(年齡1+年齡2+年齡n的多個年齡對象)總和超過了Survivor區(qū)域的50%,此時就會把年齡n(含)以上的對象都放入老年代
-
-XX:MaxTenuringThreshold:最大年齡閾值(默認15)
-
-XX:InitiatingHeapOccupancyPercent:老年代占用空間達到整堆內存閾值(默認45%),則執(zhí)行新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認有2048個region,如果有接近1000個region都是老年代的region,則可能就要觸發(fā)MixedGC了
-
-XX:G1MixedGCLiveThresholdPercent(默認85%) region中的存活對象低于這個值時才會回收該region,如果超過這個值,存活對象過多,回收的的意義不大。
-
-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(默認8次),在最后一個篩選回收階段可以回收一會,然后暫停回收,恢復系統(tǒng)運行,一會再開始回收,這樣可以讓系統(tǒng)不至于單次停頓時間過長。
-
-XX:G1HeapWastePercent(默認5%): gc過程中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基于復制算法進行的,都是把要回收的Region里的存活對象放入其他Region,然后這個Region中的垃圾對象全部清理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閑出來的Region數(shù)量達到了堆內存的5%,此時就會立即停止混合回收,意味著本次混合回收就結束了。
適用場景
-
50%以上的堆被存活對象占用
-
對象分配和晉升的速度變化非常大
-
垃圾回收時間特別長,超過1秒
-
8GB以上的堆內存(建議值)
-
停頓時間是500ms以內
G1優(yōu)化建議
假設參數(shù) -XX:MaxGCPauseMills 設置的值很大,導致系統(tǒng)運行很久,年輕代可能都占用了堆內存的60%了,此時才觸發(fā)年輕代gc。
那么存活下來的對象可能就會很多,此時就會導致Survivor區(qū)域放不下那么多的對象,就會進入老年代中。或者是你年輕代gc過后,存活下來的對象過多,導致進入Survivor區(qū)域后觸發(fā)了動態(tài)年齡判定規(guī)則,達到了Survivor區(qū)域的50%,也會快速導致一些對象進入老年代中。
所以這里核心還是在于調節(jié) -XX:MaxGCPauseMills 這個參數(shù)的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc過后的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發(fā)mixed gc.
優(yōu)化小Demo
像kafka來這種支撐高并發(fā)的消息系統(tǒng)來說說,每秒處理幾萬甚至幾十萬消息時很正常的,一般來說部署kafka需要用大內存機器(比如64G),也就是說可以給年輕代分配個三四十G的內存用來支撐高并發(fā)處理 。
這里就涉及到一個問題了,我們以前常說的對于eden區(qū)的young gc是很快的,這種情況下它的執(zhí)行還會很快嗎?很顯然,不可能,因為內存太大,處理還是要花不少時間的,假設三四十G內存回收可能最快也要幾秒鐘,按kafka這個并發(fā)量放滿三四十G的eden區(qū)可能也就一兩分鐘吧,那么意味著整個系統(tǒng)每運行一兩分鐘就會因為young gc卡頓幾秒鐘沒法處理新消息,顯然是不行的。
那么對于這種情況如何優(yōu)化了,我們可以使用G1收集器,設置 -XX:MaxGCPauseMills 為50ms,假設50ms能夠回收三到四個G內存,然后50ms的卡頓其實完全能夠接受,用戶幾乎無感知,那么整個系統(tǒng)就可以在卡頓幾乎無感知的情況下一邊處理業(yè)務一邊收集垃圾。
G1天生就適合這種大內存機器的JVM運行,可以比較完美的解決大內存垃圾回收時間過長的問題。
參考
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
https://www.oracle.com/technical-resources/articles/java/g1gc.html
https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573
總結
以上是生活随笔為你收集整理的JVM - G1初探的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM - CMS深度剖析
- 下一篇: JVM - ZGC初探