1. 程式人生 > 實用技巧 >UseG1GC垃圾回收技術解析

UseG1GC垃圾回收技術解析

介紹

G1 GC,全稱Garbage-First Garbage Collector,通過-XX:+UseG1GC引數來啟用。G1收集器是工作在堆內不同分割槽上的收集器,分割槽既可以是年輕代也可以是老年代,同一個代的分割槽不需要連續。並且每個代分割槽的數量是可以動態調整的。為老年代設定分割槽的目的是老年代裡有的分割槽垃圾多,有的分割槽垃圾少,這樣在回收的時候可以專注於收集垃圾多的分割槽,這也是G1名稱的由來。不過這個演算法並不適合新生代垃圾收集,因為新生代的垃圾收集演算法是複製演算法,但是新生代也使用了分割槽機制主要是因為便於代大小的調整。
G1 GC是設計用來取代CMS的,同CMS相比G1有以下優勢:

1、可預測的停頓模型
2、避免了CMS的垃圾碎片
3、超大堆的表現更出色

G1關鍵概念

Region

G1裡面的Region的概念不同於傳統的垃圾回收演算法中的分割槽的概念。G1預設把堆記憶體分為1024個分割槽,後續垃圾收集的單位都是以Region為單位的。Region是實現G1演算法的基礎,每個Region的大小相等,通過-XX:G1HeapRegionSize引數可以設定Region的大小。如下圖所示:

圖中的E代表是Eden區,S代表Survivor,O代表Old區,H代表humongous表示巨型物件(大小大小Region空間一半的物件)。從圖中可以看出各個區域邏輯上並不是連續的。並且一個Region在某一個時刻是Eden,在另一個時刻就可能屬於老年代。G1在進行垃圾清理的時候就是將一個Region的物件拷貝到另外一個Region中。

SATB

SATB的全稱是Snapchat-At-The_Beginning。SATB是維持併發GC的一種手段。G1併發的基礎就是SATB。SATB可以理解成在GC開始之前對堆記憶體裡的物件做一次快照,此時活的物件就認為是活的,從而形成一個物件圖。在GC收集的時候,新生代的物件也認為是活的物件,除此之外其他不可達的物件都認為是垃圾物件。
如何找到在GC的過程中分配的物件呢?每個region記錄著兩個top-at-mark-start(TAMS)指標,分別為prevTAMS和nextTAMS。在TAMS以上的物件就是新分配的,因而被視為隱式marked。通過這種方式我們就找到了在GC過程中新分配的物件,並把這些物件認為是活的物件。

解決了物件在GC過程中分配的問題,那麼在GC過程中引用發生變化的問題怎麼解決呢, G1給出的解決辦法是通過Write Barrier。Write Barrier就是對引用欄位進行賦值做了環切。通過Write Barrier就可以瞭解到哪些引用物件發生了什麼樣的變化。

RSet

RSet全稱是Remember Set,每個Region中都有一個RSet,記錄的是其他Region中的物件引用本Region物件的關係(誰引用了我的物件)。G1裡面還有另外一種資料結構就Collection Set(CSet),CSet記錄的是GC要收集的Region的集合,CSet裡的Region可以是任意代的。在GC的時候,對於old->young和old->old的跨代物件引用,只要掃描對應的CSet中的RSet即可。

停頓預測模型

G1收集器突出表現出來的一點是通過一個停頓預測模型來根據使用者配置的停頓時間來選擇CSet的大小,從而達到使用者期待的應用程式暫停時間。通過-XX:MaxGCPauseMillis引數來設定。這一點有點類似於ParallelScavenge收集器。關於停頓時間的設定並不是越短越好。設定的時間越短意味著每次收集的CSet越小,導致垃圾逐步積累變多,最終不得不退化成Serial GC;停頓時間設定的過長,那麼會導致每次都會產生長時間的停頓,影響了程式對外的響應時間。

#G1回收的過程
G1垃圾回收分為兩個階段:
1、全域性併發標記階段(Global Concurrent marking)
2、拷貝存活物件階段(evacuation)

全域性併發標記階段

全域性併發標記階段是基於SATB的,與CMS有些類似,但是也有不同的地方,主要的幾個階段如下:
初始標記:該階段會STW。掃描根集合,將所有通過根集合直達的物件壓入掃描棧,等待後續的處理。在G1中初始標記階段是藉助Young GC的暫停進行的,不需要額外的暫停。雖然加長了Young GC的暫停時間,但是從總體上來說還是提高的GC的效率。
併發標記:該階段不需要STW。這個階段不斷的從掃描棧中取出物件進行掃描,將掃描到的物件的欄位再壓入掃描棧中,依次遞迴,直到掃描棧為空,也就是說trace了所有GCRoot直達的物件。同時這個階段還會掃描SATB write barrier所記錄下的引用。
最終標記:也叫Remark,這個階段也是STW的。這個階段會處理在併發標記階段write barrier記錄下的引用,同時進行弱引用的處理。這個階段與CMS的最大的區別是CMS在這個階段會掃描整個根集合,Eden也會作為根集合的一部分被掃描,因此耗時可能會很長。
清理:該階段會STW。清點和重置標記狀態。這個階段有點像mark-sweep中的sweep階段,這個階段並不會實際上去做垃圾的收集,只是去根據停頓模型來預測出CSet,等待evacuation階段來回收。

拷貝存活物件階段

Evacuation階段是全暫停的。該階段把一部分Region裡的活物件拷貝到另一部分Region中,從而實現垃圾的回收清理。Evacuation階段從第一階段選出來的Region中篩選出任意多個Region作為垃圾收集的目標,這些要收集的Region叫CSet,通過RSet實現。
篩選出CSet之後,G1將並行的將這些Region裡的存活物件拷貝到其他Region中,這點類似於ParalledScavenge的拷貝過程,整個過程是完全暫停的。關於停頓時間的控制,就是通過選擇CSet的數量來達到控制時間長短的目標。

G1的收集模式:

YoungGC:收集年輕代裡的Region
MixGC:年輕代的所有Region+全域性併發標記階段選出的收益高的Region
無論是YoungGC還是MixGC都只是併發拷貝的階段。

分代G1模式下選擇CSet有兩種子模式,分別對應YoungGC和mixedGC:
YoungGC:CSet就是所有年輕代裡面的Region
MixedGC:CSet是所有年輕代裡的Region加上在全域性併發標記階段標記出來的收益高的Region

G1的執行過程是這樣的,會在Young GC和Mix GC之間不斷的切換執行,同時定期的做全域性併發標記,在實在趕不上回收速度的情況下使用Full GC(Serial GC)。初始標記是搭在YoungGC上執行的,在進行全域性併發標記的時候不會做Mix GC,在做Mix GC的時候也不會啟動初始標記階段。當MixGC趕不上物件產生的速度的時候就退化成Full GC,這一點是需要重點調優的地方。

G1最佳實踐

在使用G1垃圾收集器的時候遵循以下實踐可以少走不少彎路:

不斷調優暫停時間指標

通過XX:MaxGCPauseMillis=x可以設定啟動應用程式暫停的時間,G1在執行的時候會根據這個引數選擇CSet來滿足響應時間的設定。一般情況下這個值設定到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設定成50ms就不太合理。暫停時間設定的太短,就會導致出現G1跟不上垃圾產生的速度。最終退化成Full GC。所以對這個引數的調優是一個持續的過程,逐步調整到最佳狀態。

不要設定新生代和老年代的大小

G1收集器在執行的時候會調整新生代和老年代的大小。通過改變代的大小來調整物件晉升的速度以及晉升年齡,從而達到我們為收集器設定的暫停時間目標。設定了新生代大小相當於放棄了G1為我們做的自動調優。我們需要做的只是設定整個堆記憶體的大小,剩下的交給G1自己去分配各個代的大小。

關注Evacuation Failure

Evacuation Failure類似於CMS裡面的晉升失敗,堆空間的垃圾太多導致無法完成Region之間的拷貝,於是不得不退化成Full GC來做一次全域性範圍內的垃圾收集。

G1常用引數

引數/預設值 含義

-XX:+UseG1GC 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=200 設定期望達到的最大GC停頓時間指標(JVM會盡力實現,但不保證達到)
-XX:InitiatingHeapOccupancyPercent=45 啟動併發GC週期時的堆記憶體佔用百分比. G1之類的垃圾收集器用它來觸發併發GC週期,基於整個堆的使用率,而不只是某一代記憶體的使用比. 值為 0 則表示”一直執行GC迴圈”. 預設值為 45.
-XX:NewRatio=n 新生代與老生代(new/old generation)的大小比例(Ratio). 預設值為 2.
-XX:SurvivorRatio=n eden/survivor 空間大小的比例(Ratio). 預設值為 8.
-XX:MaxTenuringThreshold=n 提升年老代的最大臨界值(tenuring threshold). 預設值為 15.
-XX:ParallelGCThreads=n 設定垃圾收集器在並行階段使用的執行緒數,預設值隨JVM執行的平臺不同而不同.
-XX:ConcGCThreads=n 併發垃圾收集器使用的執行緒數量. 預設值隨JVM執行的平臺不同而不同.
-XX:G1ReservePercent=n 設定堆記憶體保留為假天花板的總量,以降低提升失敗的可能性. 預設值是 10.
-XX:G1HeapRegionSize=n 使用G1時Java堆會被分為大小統一的的區(region)。此引數可以指定每個heap區的大小. 預設值將根據 heap size 算出最優解. 最小值為 1Mb, 最大值為 32Mb.

G1日誌分析

//新生代GC
2018-05-03T10:21:43.209-0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0035356 secs]  //初始標記,耗時0.0035秒
   [Parallel Time: 2.4 ms, GC Workers: 8]  //並行8個執行緒,耗時2.4ms
      [GC Worker Start (ms): Min: 813.1, Avg: 813.7, Max: 813.9, Diff: 0.7]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 1.1, Max: 1.5, Diff: 1.5, Sum: 9.1]   //每個掃描root的執行緒耗時
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]   //更新RS的耗時,G1中每塊區域都有一個RS與之對應,RS記錄了該區域被其他區域引用的物件。回收時,就把RS作為根集的一部分,從而加快回收
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]  //Processed Buffers就是記錄引用變化的快取空間
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]   //掃描RS
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]  //根掃描耗時
      [Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 1.3, Diff: 1.3, Sum: 3.6] //物件拷貝
      [Termination (ms): Min: 0.0, Avg: 0.2, Max: 0.2, Diff: 0.2, Sum: 1.2]   
         [Termination Attempts: Min: 1, Avg: 1.8, Max: 4, Diff: 3, Sum: 14]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 1.6, Avg: 1.8, Max: 2.3, Diff: 0.8, Sum: 14.1]   //GC執行緒耗時
      [GC Worker End (ms): Min: 815.4, Avg: 815.4, Max: 815.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]   //清空CardTable耗時,RS是依賴CardTable記錄區域存活物件的
   [Other: 1.1 ms]
      [Choose CSet: 0.0 ms]   //選取CSet
      [Ref Proc: 0.9 ms]  //弱引用、軟引用的處理耗時
      [Ref Enq: 0.0 ms]   //弱引用、軟引用的入隊耗時
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]   //釋放被回收區域的耗時(包含他們的RS)
   [Eden: 5120.0K(24.0M)->0.0B(12.0M) Survivors: 0.0B->2048.0K Heap: 16.0M(50.0M)->12.4M(50.0M)]
 [Times: user=0.01 sys=0.00, real=0.01 secs] 
 //根區域掃描
2018-05-03T10:21:43.213-0800: [GC concurrent-root-region-scan-start]
2018-05-03T10:21:43.214-0800: [GC concurrent-root-region-scan-end, 0.0012422 secs]
// 併發標記
2018-05-03T10:21:43.214-0800: [GC concurrent-mark-start]
2018-05-03T10:21:43.214-0800: [GC concurrent-mark-end, 0.0004063 secs]
//重新標記又叫最終標記
2018-05-03T10:21:43.214-0800: [GC remark 2018-05-03T10:21:43.215-0800: [Finalize Marking, 0.0003736 secs] 2018-05-03T10:21:43.215-0800: [GC ref-proc, 0.0000533 secs] 2018-05-03T10:21:43.215-0800: [Unloading, 0.0007439 secs], 0.0013442 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
 //獨佔清理
2018-05-03T10:21:43.216-0800: [GC cleanup 13M->13M(50M), 0.0004002 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs]

這是一段完整的GC日誌。從整體上看,併發標記週期和混合回收的前後都有可能穿插著新生代GC。其中併發標記週期主要是回收老年代空間,當然也包含了一次新生代GC。