1. 程式人生 > >神奇的G1——Java全新垃圾回收機制

神奇的G1——Java全新垃圾回收機制

G1全稱是Garbage First Garbage Collector,使用G1的目的是簡化效能優化的複雜性。例如,G1的主要輸入引數是初始化和最大Java堆大小、最大GC中斷時間。

G1 GC由Young Generation和Old Generation組成。G1將Java堆空間分割成了若干個Region,即年輕代/老年代是一系列Region的集合,這就意味著在分配空間時不需要一個連續的記憶體區間,即不需要在JVM啟動時決定哪些Region屬於老年代,哪些屬於年輕代。因為隨著時間推移,年輕代Region被回收後,又會變為可用狀態(後面會說到的Unused Region或Available Region)了。

G1年輕代收集器是並行Stop-the-world收集器,和其他的HotSpot GC一樣,當一個年輕代GC發生時,整個年輕代被回收。G1的老年代收集器有所不同,它在老年代不需要整個老年代回收,只有一部分Region被呼叫。

G1 GC的年輕代由Eden Region和Survivor Region組成。當一個JVM分配Eden Region失敗後就觸發一個年輕代回收,這意味著Eden區間滿了。然後GC開始釋放空間,第一個年輕代收集器會移動所有的儲存物件從Eden Region到Survivor Region,這就是“Copy to Survivor”過程。

清單1所示是年輕代的回收GC輸出日誌,在這個日誌裡面,請見最後一行,年輕代新的大小是224(New Eden)+32(New Survivor)=256MB

清單1 G1回收年輕代

15.910: [GC pause (young), 0.0260410 secs]

[Parallel Time: 18.0 ms, GC Workers: 4]

[GC Worker Start (ms): Min: 15909.7, Avg: 15909.7, Max: 15909.7, Diff: 0.0]

[Ext Root Scanning (ms): Min: 5.6, Avg: 6.1, Max: 6.8, Diff: 1.2, Sum: 24.3]

[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

[Processed Buffers: Min: 0, Avg: 12.5, Max: 24, Diff: 24, Sum: 50]

[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]

[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]

[Object Copy (ms): Min: 11.1, Avg: 11.8, Max: 12.2, Diff: 1.1, Sum: 47.1]

[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

[GC Worker Total (ms): Min: 18.0, Avg: 18.0, Max: 18.0, Diff: 0.0, Sum: 72.0]

[GC Worker End (ms): Min: 15927.7, Avg: 15927.7, Max: 15927.7, Diff: 0.0] 
[Code Root Fixup: 0.1 ms]

[Code Root Migration: 0.2 ms]

[Clear CT: 0.1 ms]

[Other: 7.6 ms]

[Choose CSet: 0.0 ms]

[Ref Proc: 7.1 ms]

[Ref Enq: 0.1 ms]

[Free CSet: 0.2 ms] 
[Eden: 256.0M(256.0M)->0.0B(224.0M) Survivors: 0.0B->32.0M Heap: 256.0M(1024.0M)->35.1M(1024.0M)]

[Times: user=0.06 sys=0.02, real=0.02 secs]

當一個Java堆空間瓶頸點超過後,即堆空間耗盡,這時G1初始化老年代收集器,這個Initial-Mark階段是一個並行Stop-the-World的,它的大小和老年代以及整個Java堆大小有關。

Initial-Mark階段和下一個Young GC一起執行,一旦Initial-Mark完成,一個多執行緒並行的Marking階段開始去標記老年代所有存活的物件。當並行Marking階段完成,一個並行的Stop-the-World的階段開始去標記所有的物件(由於和Marking階段並行存在,應用程式多執行緒程式可能會丟失資料)。Remark節點結束後,G1有老年代所有Region的完整的標記資訊,如果這時老年代沒有任務存活物件,那麼下一個階段Cleanup階段就不需要額外的GC工作了。

上面的描述是關於老年代收集器的流程描述,簡要說明就是Initial-Mark-> Concurrent Root Region scanning->Concurrent Marking-> Remarking->Cleanup。

在Cleanup階段,如果G1 GC發現一個可回收的Region,不用等到下一個GC停頓可以直接回收這些Region並且加入到空閒Region的LinkedList連結串列。

CMS、Parallel、Serial GC都需要通過Full GC去壓縮老年代並在這個過程中掃描整個老年代。

因為G1的操作以Region為基礎,因此它適用於大Java堆。即便Java堆很大,大量的GC工作可以被限制在小型Region集合裡面。G1允許使用者指定停頓時間目標,G1通過自適應的堆大小來滿足這個目標。

G1 GC深度原理

G1把整個Java堆劃分為若干個區間(Regions)。每個Region大小為2的倍數,範圍在1MB-32MB之間,可能為1,2,4,8,16,32MB。所有的Region有一樣的大小,JVM生命週期內不會改變。例如-Xmx16g –Xms16g,設定16GB的堆大小,2000個Regions,則每個Region=16GB/2000=8MB。如果堆大小很大,而每個Region的大小很小,則Region數量可能會超過2000個。同樣地,很小的堆大小會導致Region數量很少。

Region型別

G1對於Region的幾個定義:

Available Region=可用的空閒Region

Eden Region = 年輕代Eden空間

Suivivor Region=年輕代Survivor空間

所有Eden和Survivor的集合=整個年輕代

Humongous Region=大物件Region

Humongous Region

大物件是指佔用大小超過一個Region50%空間的物件,這個大小包含了Java物件頭。物件頭大小在32位和64位HotSpot VM之間有差異,可以使用Java Object Layout工具確定頭大小,簡稱JOL。

當大物件開始進入排隊時,G1會調動幾個連續的有效Region存放它。第一個Region叫做“大物件開始”Region,其他Regions叫“大物件延續”Regions。如果沒有連續的可用Regions,G1會做一個Java heap的full gc去壓縮物件。大物件區間屬於老年代的一部分,它只包含一個物件,這個屬性允許G1收集一個大物件區間當並行Marking階段發現沒有物件存活時。當這個條件觸發,所有包含大物件的區間都可以立即被回收申明。

對於G1來說,一個潛在的挑戰是短生命週期的大物件可能不會被申明直到它們變成沒有被引用。JDK8U45申明瞭一個方法在年輕代回收大物件Region。

前面說過,G1 Region包括Young Region、Old Region、Humougous Region、Free Region。每個收集器單元跨越一個年輕和老年Regions。一個大物件跨越兩個收集器單元,所以大物件Region是一個連續的Region,如圖1所示。

圖1 Region跨越分佈圖1

圖片描述

接下來連續的Region叫做Continues Humongous。大物件2跨越三個連續的堆Regions,大物件3跨越了一個Region。

圖2 Region跨越分佈圖2

圖片描述

RSet

基於老年代的收集器採用Heap裡不同區域區分/隔離物件,這些不同的區域裡面的物件對應了不同年代。這樣年代收集器可以集中精力在最近分配的物件上,因為它們會發現一些物件不久會死亡。這些年代在堆裡可以被分別收集,這樣不用掃描整個Heap,可以節省時間和減小響應時間,並且存活時間長的物件不用來回複製,減少了拷貝和引用更新開銷。

為了方便收集器的獨立性,許多GC維持了每個年代的RSet。每一個RSet是一個數據結構,它維護並跟蹤收集器單元的內部引用,如G1 GC的Region一樣,減少了掃描整個Heap堆獲取資訊的耗時。當G1 GC執行了一個Stop-the-world收集(年輕代或混合代),它可以通過CSet掃描Region的RSets。一旦存活物件被移除,它們的引用立即被更新。

圖3 RSet佈局圖示例

圖片描述

從上面的圖3,我們可以看見一個年輕代Region(Region X)和兩個老年代Region(Region Y和Region Z)。Region X有一個從Region Z來的引用。這個引用被標記在了Region X的RSet裡面。我們也觀察到Region Z有兩個引用,一個來自於Region X,另一個來自於Region Y。Region Z的RSet需要標記從Region Y過來的引用,但是不需要去記住從Region X來的引用,因為年輕代是全域性被收集的。對於Region Y,最終我們可以看到從Region X來的引用,並沒有在Region Y的RSet裡記錄引用。

Mixed GC事件

隨著很多物件被提升到老年代,以及大物件進入大物件區間,整個Java堆區佔有率上升。為了避免Java堆空間溢位,JVM程序需要去初始化一個GC(不僅包含年輕代Regions,也包含增加老年代Region到混合收集器)。在混合GC事件裡,所有的年輕代Regions會被收集,同時一部分老年代Region也會被收集。

清單2 G1老年代回收

120.298: [GC pause (mixed), 0.0221810 secs]

[Parallel Time: 17.1 ms, GC Workers: 4]

[GC Worker Start (ms): Min: 120298.2, Avg: 120298.2, Max: 120298.2, Diff: 0.1]

[Ext Root Scanning (ms): Min: 5.6, Avg: 5.8, Max: 5.8, Diff: 0.2, Sum: 23.1]

[Update RS (ms): Min: 0.7, Avg: 0.9, Max: 1.0, Diff: 0.3, Sum: 3.5]

[Processed Buffers: Min: 4, Avg: 10.5, Max: 23, Diff: 19, Sum: 42]

[Scan RS (ms): Min: 1.1, Avg: 1.1, Max: 1.2, Diff: 0.1, Sum: 4.5]

[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[Object Copy (ms): Min: 9.0, Avg: 9.2, Max: 9.5, Diff: 0.4, Sum: 36.8]

[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

[GC Worker Total (ms): Min: 16.9, Avg: 17.0, Max: 17.0, Diff: 0.1, Sum: 68.0]

[GC Worker End (ms): Min: 120315.2, Avg: 120315.2, Max: 120315.2, Diff: 0.0] 
[Code Root Fixup: 0.1 ms]

[Code Root Migration: 0.1 ms]

[Clear CT: 0.1 ms]

[Other: 4.7 ms]

[Choose CSet: 0.1 ms]

[Ref Proc: 4.1 ms]

[Ref Enq: 0.0 ms]

[Free CSet: 0.3 ms] 
[Eden: 224.0M(224.0M)->0.0B(224.0M) Survivors: 32.0M->32.0M Heap: 547.5M(1024.0M)->297.3M(1024.0M)]

[Times: user=0.08 sys=0.00, real=0.02 secs]

Full Garbage Collections

G1內部,前面提到的混合GC是非常重要的釋放記憶體機制,它避免了G1出現Region沒有可用的情況,否則就會觸發Full GC事件。

CMS、Parallel、Serial GC都需要通過Full GC去壓縮老年代並在這個過程中掃描整個老年代。G1的Full GC演算法和Serial GC收集器完全一致。當一個Full GC發生時,整個Java堆執行一個完整的壓縮,這樣確保了最大的空餘記憶體可用。G1的Full GC是一個單執行緒,它可能引起一個長時間的停頓時間,G1的設計目標是減少Full GC,滿足應用效能目標。

G1 GC常用引數

我在這裡所羅列的引數的預設值都是基於JDK8u45,所以可能後續的JDK版本會有些值不一樣,這個讀者可以直接通過JDK的官方幫助文件獲取最新預設值資訊。

-XX:+UseG1GC:啟用G1 GC。JDK7和JDK8要求必須顯示申請啟動G1 GC,JDK可能會設定G1 GC為預設GC選項,也有可能會退到早期的Parallel GC,這個也請關注吧,JDK9預計2017年正式釋出;

-XX:G1NewSizePercent:初始年輕代佔整個Java Heap的大小,預設值為5%;

-XX:G1MaxNewSizePercent:最大年輕代佔整個Java Heap的大小,預設值為60%;

-XX:G1HeapRegionSize:設定每個Region的大小,單位MB,需要為1,2,4,8,16,32其一,預設是堆記憶體的1/2000。前面我們講過大物件概念,如果這個值設定比較大,那麼大物件就可以進入Region了,同樣地,這樣做的壞處是直接干預了各年齡代的分配大小;

-XX:ConcGCThreads:與Java應用一起執行的GC執行緒數量。預設是Java執行緒的1/4。減少這個引數的數值可能會提升並行回收的效率,即提高系統內部吞吐量(系統是一個整體,CPU資源大家都需要佔用),不過如果這個數值過低,也會導致並行回收機制耗時加長;

-XX:+InitiatingHeapOccupancyPercent(簡稱IHOP):G1內部並行迴圈啟動的設定值,預設為Java Heap的45%。這個可以理解為老年代空間佔用的空間,GC收集後需要低於45%的佔用率。這個值主要是為了決定在什麼時間啟動老年代的並行回收迴圈,這個迴圈從初始化並行回收開始,可以避免Full GC的發生;

-XX:G1HeapWastePercent:G1不會回收的記憶體大小,預設是堆大小的5%。GC會收集所有的Region,如果值達到5%,就會停下來不再收集了; -XX:G1MixedGCCountTarget:設定並行迴圈之後需要有多少個混合GC啟動,預設值是8個。老年代Regions的回收時間通常比年輕代的收集時間要長一些,所以如果混合收集器比較多,可以允許G1延長老年代的收集時間;

-XX:+G1PrintRegionLivenessInfo:這個引數需要和-XX:+UnlockDiagnosticVMOptions配合啟動,這可以理解,它們本身就是屬於VM的除錯資訊。如果開啟了,VM會列印堆記憶體裡每個Region的存活物件資訊。這個資訊在標記迴圈結束後可以打印出來;

-XX:G1ReservePercent:這個值是為了保留一些空間用於年代之間的提升,預設值是堆空間的10%。注意這個空間保留後就不會用在年輕代了,大家可以看到GC日誌裡輸出顯示,我們大量執行的是年輕代回收,所以如果你的應用裡面有比較大的堆記憶體空間、比較多的大物件存活,那還是減少一點保留空間吧,這樣會給年輕代更多的預留空間、GC之間更長的處理時間;

-XX:+G1SummarizeRSetStats:這個也是一個VM的除錯資訊。如果啟用,會在VM推出的時候打印出RSets的詳細總結資訊。如果啟用-XX:G1SummaryRSetStatsPeriod引數,就會階段性地列印RSets資訊;

-XX:+G1TraceConcRefinement:這個也是一個VM的除錯資訊。如果啟用,並行回收階段的日誌就會被詳細打印出來;

-XX:+GCTimeRatio:大家知道,GC的有些階段是需要Stop-the-World,即停止應用執行緒的,這個引數就是計算花在Java應用執行緒上和花在GC執行緒上的時間比率,預設是9。這個引數主要的目的是讓使用者可以控制花在應用上的時間,G1的計算公式是100/(1+GCTimeRatio),這樣如果採用9,則最多10%的時間會花在GC工作上面。Parallel GC的預設值是99,表示1%的時間被用在GC上面,這是因為Parallel GC貫穿整個GC,而G1則根據Region來進行劃分,不需要全域性性掃描Java Heap;

-XX:+UseStringDeduplication:手動開啟Java String物件的分割工作,這個是JDK8u20之後新增的引數,主要用於相同String避免重複申請記憶體,節約Region的使用;

-XX:MaxGCPauseMills:G1停止執行的一個目標值,單位是毫秒,預設是200毫秒,這個值不一定真的會達到。這個引數會通過控制年輕代的大小來實現目標。

結束語

我們知道垃圾回收機制一直是Java著力加強點,G1 GC通過引入許多不同的方式解決了Parallel、Serial、CMS GC的許多缺點。G1通過將Heap劃分為多個Region,可以讓G1操作可以在一個Region裡面執行而不是整個Java堆或者整個年代(Generation)。本文作者一直的觀點是Practice by self,幾乎所有的應用程式都有自身的獨特性,所以不能給出千篇一律的GC配置單,需要的是讀者自己的反覆試驗、比對結果、確定方案。