G1收集器(-XX:+UseG1GC)
G1 把堆劃分為許多大小相同的 Region(默認 1~32 MB,一個堆通常包含上千個 Region,JVM目標是不超過2048個Region(JVM源碼里TARGET_REGION_NUMBER 定義))
不再是傳統的 Eden、Survivor、Old 靜態分代模型,而是用邏輯分區標記 Region 類型,采用復制算法針對每個區域進行垃圾回收,同樣也支持動態的調整內存大小。
分代
- 年輕代 Region(Eden + Survivor)
- 老年代 Region
- 大對象 Region(Humongous)
每個Region的屬性可動態調整
一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當然也可以用參數"-XX:G1HeapRegionSize"手動指定Region大小,但是推薦默認的計算方式。
默認年輕代對堆內存的占比是5%,如果堆大小為4096M,那么年輕代占據200MB左右的內存,對應大概是100個Region,可以通過“-XX:G1NewSizePercent”設置新生代初始占比,在系統運行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”調整。
年輕代中的Eden和Survivor比例也是默認8:1:1
回收步驟
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中,這種不會像CMS那樣回收完因為有很多內存碎片還需要整理一次,G1采用復制算法回收幾乎不會有太多內存碎片(CMS回收階段是跟用戶線程一起并發執行的,G1因為內部實現太復雜暫時沒實現并發回收)
**G1收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1會優先選擇后面這個Region回收。**這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限時間內可以盡可能高的收集效率。
大對象處理
不同的是對大對象的處理,G1有專門分配大對象的Region叫Humongous區。在G1中,大對象的判定規則就是一個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。
Humongous區專門存放短期巨型對象,不用直接進老年代,可以節約老年代的空間,避免因為老年代空間不夠的GC開銷。
特點
- 并行與并發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式讓java程序繼續執行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
- 空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基于“標記整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的。
- 可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1
除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段(通過參數"-XX:MaxGCPauseMillis"指定)內完成垃圾收集。
GC回收
Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區一并回收。
-
Young GC(年輕代收集)
回收年輕代中的 Eden 和 Survivor 區。G1會計算下現在Eden區回收大概要多久時間,如果回收時間遠遠小于參數 -XX:MaxGCPauseMillis 設定的值,那么增加年輕代的region,繼續給新對象存放,不會馬上做Young GC,直到下一次Eden區放滿,G1計算回收時間接近參數 -XX:MaxGCPauseMillis 設定的值,那么就會觸發Young GC -
Mixed GC(混合回收)
回收年輕代和部分老年代垃圾多的 Region。老年代的堆占有率達到參數(-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 這個參數的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc過后的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc.
什么場景適合使用G1
50%以上的堆被存活對象占用
對象分配和晉升的速度變化非常大
垃圾回收時間特別長,超過1秒
8GB以上的堆內存(建議值)
停頓時間是500ms以內
ZGC收集器(-XX:+UseZGC)
ZGC目標
- 支持TB量級的堆。我們生產環境的硬盤還沒有上TB呢,這應該可以滿足未來十年內,所有JAVA應用的需求了吧。
- 最大GC停頓時間不超10ms。目前一般線上環境運行良好的JAVA應用Minor GC停頓時間在10ms左右,Major GC一般都需要100ms以上(G1可以調節停頓時間,但是如果調的過低的話,反而會適得其反),之所以能做到這一點是因為它的停頓時間主要跟Root掃描有關,而Root數量和堆大小是沒有任何關系的。
- 奠定未來GC特性的基礎。
- 最糟糕的情況下吞吐量會降低15%。這都不是事,停頓時間足夠優秀。至于吞吐量,通過擴容分分鐘解決。
Oracle官方提到了它最大的優點是:它的停頓時間不會隨著堆的增大而增長!
不分代(暫時)
單代,即ZGC「沒有分代」。為什么ZGC就不分代呢?因為分代實現起來麻煩,作者就先實現出一個比較簡單可用的單代版本,后續會優化。
ZGC內存布局
ZGC收集器是一款基于Region內存布局的, 暫時不設分代的, 使用了讀屏障、 顏色指針等技術來實現可并發的標記-整理算法的, 以低延遲為首要目標的一款垃圾收集器。
- 小型Region(Small Region) : 容量固定為2MB, 用于放置小于256KB的小對象。
- 中型Region(Medium Region) : 容量固定為32MB, 用于放置大于等于256KB但小于4MB的對象。
- 大型Region(Large Region) : 容量不固定, 可以動態變化, 但必須為2MB的整數倍, 用于放置4MB或以上的大對象。 每個大型Region中只會存放一個大對象雖然名字叫作“大型Region”, 但它的實際容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的實現中是不會被重分配, 因為復制一個大對象的代價非常高昂。
ZGC針對NUMA深度優化
NUMA對應的有UMA,UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture。通過感知和優化內存分配、線程調度,盡量讓線程訪問本地節點內存,減少遠程訪問,提高性能和吞吐。
ZGC運作過程
- 并發標記(Concurrent Mark):
與G1一樣,并發標記是遍歷對象圖做可達性分析的階段,它的初始標記(Mark Start)和最終標記(Mark End)也會出現短暫的停頓,與G1不同的是,** ZGC的標記是在指針上而不是在對象上進行的, 標記階段會更新顏色指針中的Marked 0、 Marked 1標志位。** - 并發預備重分配(Concurrent Prepare for Relocate):
根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護成本。 - 并發重分配(Concurrent Relocate):
重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活對象復制到新的Region上,并為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關系。ZGC收集器能僅從引用上就明確得知一個對象是否處于重分配集之中,如果用戶線程此時并發訪問了位于重分配集中的對象,這次訪問將會被預置的內存屏障(讀屏障(見下面詳解))所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。 - 并發重映射(Concurrent Remap):
重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用,但是ZGC中對象引用存在“自愈”功能,所以這個重映射操作并不是很迫切。ZGC把并發重映射階段要做的工作合并到了下一次垃圾收集循環中的并發標記階段里完成。一旦所有指針都被修正之后, 原來記錄新舊對象關系的轉發表就可以釋放掉了。
顏色指針
以前的垃圾回收器的GC信息都保存在對象頭中,而ZGC的GC信息保存在指針中。
每個對象有一個64位指針,這64位被分為:
- 18位:預留給以后使用;
- 1位:Finalizable標識,此位與并發引用處理有關,它表示這個對象只能通過finalizer才能訪問;
- 1位:Remapped標識,設置此位的值后,對象未指向relocation set中(relocation
set表示需要GC的Region集合); - 1位:Marked1標識;
- 1位:Marked0標識,和上面的Marked1都是標記對象用于輔助GC;
- 42位:對象的地址(所以它可以支持2^42=4T內存)
** 為什么有2個mark標記?**
每一個GC周期開始時,會交換使用的標記位,使上次GC周期中修正的已標記狀態失效,所有引用都變成未標記。
GC周期1:使用mark0, 則周期結束所有引用mark標記都會成為01。
GC周期2:使用mark1, 則期待的mark標記10,所有引用都能被重新標記。
對配置ZGC后對象指針分析我們可知,對象指針必須是64位,那么ZGC就無法支持32位操作系統,同樣的也就無法支持壓縮指針了(CompressedOops,壓縮指針也是32位)。
** 顏色指針的三大優勢:**
- 一旦某個Region的存活對象被移走之后,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正后才能清理,這使得理論上只要還有一個空閑Region,ZGC就能完成收集。因為還有轉發表維護轉發關系
- 顏色指針可以大幅減少在垃圾收集過程中內存屏障的使用數量,ZGC只使用了讀屏障。
- 顏色指針具備強大的擴展性,它可以作為一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便日后進一步提高性能。
讀屏障
之前的GC都是采用Write Barrier,這次ZGC采用了完全不同的方案讀屏障,這個是ZGC一個非常重要的特性。
在標記和移動對象的階段,每次「從堆里對象的引用類型中讀取一個指針」的時候,都需要加上一個Load Barriers。
判斷對象是Bad Color還是Good Color的依據是什么呢?就是根據上一段提到的Colored Pointers的4個顏色位。當加上讀屏障時,根據對象指針中這4位的信息,就能知道當前對象是Bad/Good Color了。
低42位指針可以支持4T內存,那么能否通過預約更多位給對象地址來達到支持更大內存的目的呢?答案肯定是不可以。因為目前主板地址總線最寬只有48bit,4位是顏色位,就只剩44位了,所以受限于目前的硬件,ZGC最大只能支持16T的內存,JDK13就把最大支持堆內存從4T擴大到了16T。
ZGC存在的問題
ZGC最大的問題是浮動垃圾。ZGC的停頓時間是在10ms以下,但是ZGC的執行時間還是遠遠大于這個時間的。假如ZGC全過程需要執行10分鐘,在這個期間由于對象分配速率很高,將創建大量的新對象,這些對象很難進入當次GC,所以只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
解決方案
目前唯一可行的辦法是增加堆容量,為程序爭取更多緩沖時間,但這只能暫時緩解問題。要從根本上解決,必須引入分代收集機制:將新創建的對象集中存放在特定區域,并針對該區域實施更頻繁、更高效的回收策略。
ZGC參數設置
啟用ZGC比較簡單,設置JVM參數即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」。
ZGC觸發時機
ZGC目前有4中機制觸發GC:
- 定時觸發,默認為不使用,可通過ZCollectionInterval參數配置。
- 預熱觸發,最多三次,在堆內存達到10%、20%、30%時觸發,主要時統計GC時間,為其他GC機制使用。
- 分配速率,基于正態分布統計,計算內存99.9%可能的最大分配速率,以及此速率下內存將要耗盡的時間點,在耗盡之前觸發GC(耗盡時間 - 一次GC最大持續時間 - 一次GC檢測周期時間)。
- 主動觸發,(默認開啟,可通過ZProactive參數配置) 距上次GC堆內存增長10%,或超過5分鐘時,對比距上次GC的間隔時間跟(49 * 一次GC的最大持續時間),超過則觸發。
選擇垃圾收集器
- 優先調整堆的大小讓服務器自己來選擇
- 如果內存小于100M,使用串行收集器
- 如果是單核,并且沒有停頓時間的要求,串行或JVM自己選擇
- 如果允許停頓時間超過1秒,選擇并行或者JVM自己選
- 如果響應時間最重要,并且不能超過1秒,使用并發收集器
- 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,幾百G以上用ZGC
下圖有連線的可以搭配使用
JDK 1.8默認使用 Parallel和Parallel Old
JDK 1.9默認使用 G1