详解G1垃圾收集器
G1(Garbage-First)作為繼CMS之后新一代面向服務器的垃圾收集器,它已經不再嚴格按照之前老年代和新生代的劃分來進行垃圾收集,即它是一個老年代和新生代共用的垃圾收集器。
G1更多是在多處理器(或多核)以及大內存的機器上發揮優勢,在滿足指定GC停頓時間要求的同時,還具備高吞吐量的能力。
這篇文章主要從G1的設計理念和垃圾回收過程來詳細介紹。
一、設計思想
在《GC收集算法與GC收集器》這篇文章中介紹了JVM中經典的垃圾收集器,這些垃圾收集器的共性是在整個垃圾收集過程中,一定會發生Stop The World,并且STW的時間是根據垃圾標記所需要的時間來確定,可能依然會存在某次垃圾收集時,STW的時間過長的問題,導致這個問題的原因在于經典的垃圾收集器都是對整個新生代或老年代進行垃圾回收,要掃描的對象太多了。
但STW又是每個垃圾收集器都不可避免的,垃圾收集器的發展就是為了能夠盡量縮短STW的時間。
G1采用了開創性的局部收集的設計思路和以Region為基本單位的內存布局方式,它將Java堆空間劃分成多個大小相等的獨立區域(Region),JVM目標是總共不超過2048個Region(由JVM源碼參數TARGET_REGION_NUMBER定義),雖然可以超過該值,但不推薦。
通常Region的大小等于堆空間總大小除以Region的個數,比如堆空間大小為4096MB,總共有2048個Region,那么每個Region的大小為2MB,也可以通過參數-XX:G1HeapRegionSize來指定Region的大小,假設參數值為4MB,那么堆空間就只有1024個Region了,一般推薦默認的計算方式。
G1對應的堆空間的內存布局如下所示:
G1雖然拋棄了將新生代和老年代作為整塊內存空間的方式,但依然保留了新生代和老年代的概念。只是老年代和新生代的內存空間不再是物理連續的了,它們都是Region的集合。
G1將所有Region分為四種類型:Eden、Survivor、Old、Humongous。
默認新生代的Region內存占堆空間的5%,如果堆空間大小為4096MB,那么新生代占用200MB左右的內存,按照每個Region為2MB,對應就是100個Region。也可以通過參數-XX:G1NewSizePercent設置新生代初始占比。
在系統運行過程中,JVM會動態地給年輕代增加更多的Region,但新生代的占比最多不會超過60%,可以通過參數-XX:G1MaxNewSizePercent設置。Region的區域類型是動態變化的,可能之前是年輕代,經過了垃圾回收之后就變成了老年代。
G1中的新生代依然與經典垃圾收集器中一樣,分為Eden區和Survivor區,默認比例也是8:1:1,如果新生代有100個Region,那么就是Eden區占用80個,兩個Survivor區各占用10個。
G1收集器對于對象從新生代轉移到老年代與CMS等經典垃圾收集器是一樣的,但對于大對象的處理有所不同。G1為大對象的內存分配專門設計了一個Humongous類型的Region,而不再是讓對象直接進入老年代的Region。
在G1中,大對象的判斷是超過一個Region大小的50%,按照每個Region大小為2MB來計算,只要對象超過了1MB,就會被放入到Humongous的Region中,如果一個對象太大,一個Region放不下,可能會存在跨多個Region來存放。
在進行Full GC的時候除了要收集新生代和老年代的Region外,還會將Humongous的Region一并進行回收。
與之前的經典垃圾收集器不同,G1可以由用戶自己去設置STW的最大時間,然后G1會根據STW的時間來進行內存回收,這個STW的時間包含了并發標記、最終標記和篩選回收三個階段STW的總時間。
二、垃圾回收過程
2.1 G1垃圾收集過程
G1在進行垃圾收集的時候,會根據每個Region預計垃圾收集所需時間與預計回收內存大小的占比來選擇對哪些區域進行回收,也就是不再有Minor GC/Yong GC和Major GC/Full GC的概念,而是采用一種Mixed GC的方式,即混合回收的GC方式。
G1的垃圾收集(主要指Mixed GC)過程可以分為下面幾個步驟:
-
初始標記
需要暫定所有線程,即STW,并記錄下GC Roots能直接引用的對象,速度很快。與CMS的初始標記一樣
-
并發標記
可以與應用線程一起工作,進行可達性分析,與CMS的并發標記一樣
-
最終標記
需要暫定所有線程(STW),根據三色標記算法修復一些引用的狀態,與CMS的重新標記是一樣的
-
篩選回收
篩選回收階段會對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓STW時間(可以通過參數 -XX:MaxGCPauseMillis設置)來制定回收計劃。
比如此時有1000個Region都滿了,但根據用戶設置的STW時間,本次垃圾回收只能停頓200毫秒,那么通過之前的回收成本計算,200毫秒只能回收600個Region的內存空間,那么G1就會只回收這600個Region(Collection Set,要回收的集合)的內存空間,盡量把GC的停頓時間控制在用戶指定的停頓時間內。
在回收的時候,使用的是復制算法,將一個Region中的存活對象移動到另一個空的Regin中,然后將之前的Region內存空間清空,G1就不需要像CMS那樣回收完內存后因為有很多脆片還要進行整理,采用復制算法幾乎不會有內存碎片。
CMS在并發清理階段,垃圾收集線程是可以與用戶線程一起并發執行,但G1因為內部實現太復雜就沒有實現并行回收,不過到了ZGC就實現了并發收集。
G1的垃圾收集過程大概如下:
在篩選回收階段對各個Region進行回收價值和成本進行排序,這句話怎么理解?
比如現在有Region1、Region2和Region3三個區域,其中Region1預計可以回收1.5MB內存,預計耗時2MS;Region2預計可以回收1MB內存,預計耗時1MS;Region3預計可以回收0.5MB內存,預計耗時1MS。那么Region1、Region2和Region3各自的回收價值與成本比值分別是:0.75、1和0.5。
比值越高說明同樣的付出,收益越高,如果此時只能回收一個Region的內存空間,G1就會選擇Region2進行回收。這種方式保證了G1收集器在有限的時間內盡可能地提高收集效率。
在《GC收集器》這篇文章介紹CMS底層算法的時候,遺留了一個問題,在G1的最終標記和CMS的重新標記過程中,為什么G1使用原始快照(SATB),而CMS使用增量更新?
主要原因是增量更新是一種深度掃描的算法,CMS中只有一塊老年代,即使進行深度更新也沒有什么問題。但G1有很多個Region,且對象的引用可能分散在多個Region中,如果這種情況還選擇深度掃描效率就會很低了。而STAB相對增量更新就會快很多,雖然STAB可能產生浮動垃圾,但產生的浮動垃圾下一次再回收就好了,并且這部分浮動垃圾也不會很多。所以G1選擇STAB,只是將對象簡單標記成黑色,保證本次垃圾收集不進行回收就可以了,等下一次GC時再做深度更新。
2.2 G1特性
G1具備以下幾個特性:
-
并行與并發
G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。G1收集器可以通過并行和并發的方式讓應用程序繼續執行。
-
分代收集
雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
-
空間整合
與CMS的標記–清理算法不同,G1從整體來看是基于“標記-整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的。
以下面的Region1和Region2為例,Region1回收后復制到了Region2,這個過程使用的復制算法,而Region2中使用中的對象是經過整理的,也個過程使用了整理算法。
-
可預測停頓
這是G1相對于CMS的另一個大優勢,降低停頓時間是G1和CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段(通過參數-XX:MaxGCPauseMillis)內完成垃圾收集。
由用戶指定期望的停頓時間是G1收集器很強大的一個功能, 設置不同的期望停頓時間, 可使得G1在不同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這里設置的“期望值”必須是符合實際的, 不能異想天開, 畢竟G1是要凍結用戶線程來復制對象的, 這個停頓時間再怎么低也得有個限度。
它默認的停頓目標為兩百毫秒, 一般來說, 回收階段占到幾十到一百甚至接近兩百毫秒都很正常, 但如果把停頓時間調得非常低, 譬如設置為二十毫秒, 很可能出現的結果就是由于停頓目標時間太短, 導致每次選出來的回收集只占堆內存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 導致垃圾慢慢堆積。 很可能一開始收集器還能從空閑的堆內存中獲得一些喘息的時間, 但應用運行時間一長就不行了, 最終占滿堆引發Full GC反而降低性能, 所以通常把期望停頓時間設置為一兩百毫秒或者兩三百毫秒會是比較合理的。
2.3 G1垃圾收集分類
G1的垃圾收集分為YoungGC、Mixed GC和Full GC。
2.3.1 Young GC
G1與之前垃圾收集器的Young GC有所不同,并不是當新生代的Eden區放滿了就進行垃圾回收,G1會計算當前Eden區回收大概需要多久的時間,如果回收時間遠小于參數-XX:MaxGCPauseMills設定的值,那么G1就會增加年輕代的Region(可以從老年代或Humongous區劃分Region給新生代),繼續給新對象存放;直到下一次Eden區放滿,G1計算回收時間接近參數-XX:MaxGCPauseMills設定的值,那么就會觸發Young GC。
2.3.2 Mixed GC
如果老年代的堆空間內存占用達到了參數-XX:InitiatingHeapOccupancyPercent設定的值就會觸發Mixed GC,回收所有的新生代和部分老年代(根據用戶設置的GC停頓時間來確定老年代垃圾收集的先后順序)以及Humongous區。正常情況下G1的垃圾收集是先做Mixed GC,主要使用復制算法,需要把各個Region中存活的對象復制到另一個空閑的Region,如果在復制過程中發現沒有足夠的空Region放復制的對象,那么就會觸發一次Full GC。
2.3.3 Full GC
停止系統程序,然后采用單線程進行標記、清理和壓縮整理,好空閑出來一批Region來供下一次Mixed GC使用,這個過程是非常耗時的。
G1收集器的Region內存回收時,會涉及到大量跨區引用的對象,解決方式也是通過記憶集(卡表)。每個Region都要維護一個其他Region對自己內部對象的引用。CMS中只有新生代和老年代,即使出現了跨代引用,也很好解決。
但G1是把堆分成了多個Region,Region中對象的可能被多個Region引用,所以G1的卡表實現要比CMS復雜很多。
三、相關參數設置
G1的相關參數與說明如下:
| -XX:+UseG1GC | 開啟使用G1垃圾收集器 |
| -XX:ParallelGCThreads | 指定GC工作的線程數量 |
| -XX:G1HeapRegionSize | 指定分區大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分為2048個分區 |
| -XX:MaxGCPauseMillis | 目標暫停(STW)時間(默認200ms) |
| -XX:G1NewSizePercent | 新生代內存初始空間(默認整堆5%,值配置整數,比如5,默認就是百分比) |
| -XX:G1MaxNewSizePercent | 新生代內存最大空間(最大60%,值配置整數) |
| -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%,此時就會立即停止混合回收,意味著本次混合回收就結束了 |
四、優化
假設參數-XX:MaxGCPauseMills設置的值很大,導致系統運行很久,年輕代可能都占用了堆內存的60%了,此時才觸發年輕代gc。
那么存活下來的對象可能就會很多,此時就會導致Survivor區域放不下那么多的對象,就會進入老年代中。
或者是年輕代GC過后,存活下來的對象過多,導致進入Survivor區域后觸發了動態年齡判定規則,達到了Survivor區域的50%,也會快速導致一些對象進入老年代中。
所以這里核心還是在于調節-XX:MaxGCPauseMills這個參數的值,在保證他的年輕代GC別太頻繁的同時,還得考慮每次GC過后的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發Mixed GC。
什么場景適合使用G1?
- 50%以上的堆被存活對象占用
- 對象分配和晉升的速度變化非常大
- 垃圾回收時間特別長,超過1秒
- 8GB以上的堆內存(建議值)
- 停頓時間是500ms以內
五、安全點與安全區域
JVM的所有垃圾收集器在做垃圾收集時,以G1的Mixed GC為例,初始標記、并發標記等每一步都需要到達一個安全點或安全區域時才能開始執行,那么是安全點和安全區域呢?
安全點就是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態是確定的,這樣JVM就可以安全的進行一些操作,比如GC等,所以GC不是想什么時候做就立即觸發的,是需要等待所有線程運行到安全點后才能觸發。
這些特定的安全點位置主要有以下幾種:
- 方法返回之前
- 調用某個方法之后
- 拋出異常的位置
- 循環的末尾
大體實現思想是當垃圾收集需要中斷線程的時候, 不直接對線程操作, 僅僅簡單地設置一個標志位, 各個線程執行過程時會不停地主動去輪詢這個標志, 一旦發現中斷標志為真時就自己在最近的安全點上主動中斷掛起。 輪詢標志的地方和安全點是重合的。
Safe Point是對正在執行的線程設定的。
如果一個線程處于 Sleep 或中斷狀態,它就不能響應 JVM 的中斷請求,再運行到 Safe Point 上。
因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代碼片段中,引用關系不會發生變化。在這個區域內的任意地方開始 GC 都是安全的。
總結
- 上一篇: dropbear编译安装与使用
- 下一篇: Java网络爬虫(一)--使用HttpC