總結《深入理解JVM》 G1 篇

胖毛發表於2020-07-06

注:一下內容主要結合《深入理解JVM》3th總結而來。

接上一篇,我們來說說G1G1作為現在的主要的JVM GC,被作為各大網際網路主要使用的垃圾回收器,瞭解G1回回收原理和回收過程,才能幫組我們更好的定位問題,解決問題。


-XX:+UseG1GC開啟G1 GC

G1記憶體劃分

G1看起來和CMS比較類似,但是實現上有很大的不同。

傳統分代GC將整體記憶體分為幾個大的區域,比如Eden,S0,S1,Tenured等。

G1將記憶體區域分為了n個不連續的大小相同Region,Region具體的大小為1到32M,根據總的記憶體大小而定,目標是數量不超過2048個。 如下圖所示:

每個RegionG1中扮演了不同的角色,比如Eden(新生區),比如Survivor(倖存區),或者Old(老年代)

除了傳統的老年代,新生代,G1還劃分出了Humongous區域,用來存放巨大物件(humongous object,H-obj)。

對於巨大物件,值得注意的有以下幾點:

  • H-obj的定義是大於等於Region一半的物件

  • H-obj直接分配到Old gen,防止頻繁拷貝。但是H-obj的回收卻不是在Mixed GC階段,而是concurrent marking階段中的clean up過程和full GC

    這點一定注意,在調優過程中你會在GC日誌中經常發現這句

    [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0029216 secs]

    疑惑點就在於為什麼Humongous Allocation卻是引發的yong gc

    原因便是在於為了通過yong gcinitial-mark開始進行concurrent marking,進而通過clean up回收大物件

    如果想要檢視G1日誌的時候,為了方便快速達到GC的效果,你可能會直接分配一些大物件以便填滿整個堆從而引發GC,但是如果光是大物件,你可能會發現GC日誌中並沒有Mixed GC,而是頻繁的Yong GCConcurrent Marking,這便是原因

  • H-obj永遠不會被移動,雖然G1的回收演算法總體上看是基於標記-整理的,但是對於H-obj則永遠不會移動,要麼直接被回收,要麼一直存在。因此H-obj可能會導致較大的記憶體碎片進而引起頻繁的GC


G1的回收過程

G1的記憶體劃分形式,決定了G1同時需要管理新生代和老年代。根據回收區域的不同,G1分為兩種回收模式:

  • 只回收部分年輕代的記憶體:Yong GC
  • 回收所有年輕代記憶體和部分老年代記憶體: Mixed GC

mixed gc回收速度趕不上記憶體分配的速度,G1會使用單執行緒(使用Serial Old的程式碼)進行Full GC

其中,整個Yong GC過程都回STW,而Mixed GC主要包括兩個階段:第一個階段為併發標記週期(Concurrent Marking),這個過程主要進行標記,當完成併發標記後,再進行老年代的垃圾回收(Evacuation):這個過程主要複製和移動物件,整理Region


Mixed GC

按道理來說,應該先說Yong Gc,可是Yong GC貌似不是G1的回收重點,同時也沒有什麼引數可以控制Yong GC,所以這裡暫時跳過。不過需要知道一點就是Yong GC的回收過程和其他垃圾回收器差不多,也是先標記,再複製。不過整個過程都會STW,同時由於Yong GC的標記過程和後面Mixed GC中的併發標記(Concurrent Marking)的第一個階段,初始標記(initial marking)所做的工作相同,因此,Concurrent Marking的初始標記階段總是搭載著Yong GC進行。

Mixed GC分為兩個階段,第一個階段是併發標記,第二個階段是篩選回收。併發標記過程如下:

  • 初始標記(initial marking)
  • 併發標記(concurrent marking)
  • 最終標記(final marking,remarking)
  • 清理(cleanup)

這裡的清理不是清理物件(和CMS不太一樣),雖然《深入理解JVM》將清理和篩選回收併為一個過程,但是GC日誌上他們是完全分開的過程。這裡以GC日誌為準

標記完成後,G1再選擇一些區域進行篩選回收。注意,這幾個階段,是可以分開執行的,也就是說,可能得執行方式如下所示:

啟動程式
-> young GC
-> young GC
-> young GC
-> young GC + initial marking
(... concurrent marking ...)
-> young GC (... concurrent marking ...)
(... concurrent marking ...)
-> young GC (... concurrent marking ...)
-> final marking
-> cleanup
-> mixed GC
-> mixed GC
-> mixed GC
...
-> mixed GC
-> young GC + initial marking
(... concurrent marking ...)

接下來詳解介紹每個標記階段所做的工作。

初始標記(initial marking):會Stop The World ,從標記所有GC Root出發可以直接到達的物件。這個過程雖然會暫停,但是它是借用的Yong GC的暫停階段,因此沒有額外的,單獨的暫停階段。

併發標記(concurrent marking) : 併發階段。從上一個階段掃描的物件出發逐個遍歷查詢,每找到一個物件就將其標記為存活狀態。注意:此過程還會掃描SATB(併發快照)所記錄的引用。

回憶併發快照:它是一個用來解決併發過程中由於使用者修改引用關係而導致物件可能被誤標的方案。CMS使用的是增量更新,這裡G1使用的是併發快照,在併發標記開始的時候記錄所有引用關係。

最終標記(final marking,remarking) : 會STW,雖然前面的併發標記過程中掃描了SATB,但是畢竟上一個階段依然是併發過程,因此需要在併發標記完成後,再次暫停所有使用者執行緒,再次標記SATB。同時這個過程也會處理弱引用。

這三個階段都和CMS比較類似,CMS也是在最終標記階段處理弱引用。

不過CMS的最終標記階段需要重新掃描整個Yong gen,因此可能CMSremark階段會非常慢。

清理(clean up) :暫停階段。清理和重置標記狀態。用來統計每個region中的中被標記為存活的物件的數量,這個階段如果發現完全沒有活物件的region就會將其整體回收到可分配region列表中。


標記完成後,便是清理(Evacuation),這個階段是完全暫停的。它負責把一部分region裡活的物件拷貝到空的region裡面,然後回收原本的region空間,此階段可以選擇任意多個region來構成收集集合(Collection Set),選定好收集集合之後,便可以將Collection Set中的物件並行拷貝到新的region中。


明白了G1整體回收過程,接下來對比CMS我們可以看看G1是如何處理併發過程中的一些問題的:

  1. 記憶集(Remember Set): 前面說過,對於跨代引用的問題,CMS選擇了不維護新生代對老年代記憶集,因為新生代變化太快,維護起來開銷比較大,而G1的解決方案是,不管Yong GC還是Mixed GC,都會將Yong Gen加入到Collection Set中,簡單說就是要麼是隻回收新生代,要麼整個新生代和老年代一起回收,這樣就避免了新生代對老年代記憶集的維護。

    這裡只討論了新生代對老年代的引用的記憶集的維護,老年代對新生代的引用還是會維護一個記憶集的

  2. 併發過程中引用變化: 這裡在Remarking階段我們已經說了,CMS使用的增量更新的方案,而G1則是使用的併發快照(STAB snapshot-at-the-beginning

  3. 關於記憶集和併發快照的維護,G1也是通過寫屏障(write barrier)來進行維護。


G1回收日誌

talk is cheap, show me the code

以上過程基本上都是通過《深入理解JVM》和網上一些資料總結而來的,究竟是不是這樣,還是需要實際操作一下,接下來我們看看G1的回收日誌:

Yong GC

//GC 原因:分配大物件 GC型別yong gc  此次帶有併發標記的initial mark  此次GC花費0.0130262s
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0130262 secs]
   //暫停過程中,並行收集花費4.5ms  使用4個執行緒同時收集
   [Parallel Time: 4.5 ms, GC Workers: 4]
      //併發工作開始時間戳
      [GC Worker Start (ms): Min: 1046.3, Avg: 1046.3, Max: 1046.4, Diff: 0.1]
      //掃描root集合(執行緒棧、JNI、全域性變數、系統表等等)花費的時間
      [Ext Root Scanning (ms): Min: 0.9, Avg: 1.0, Max: 1.2, Diff: 0.3, Sum: 4.0]
      //更新Remember Set的時間
      //由於Remember Set的維護是通過寫屏障結合緩衝區實現的,這裡Update RS就是
      //處理完緩衝區裡的元素的時間,用來保證當前Remember Set是最新
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
           //Update RS 過程中處理了多少緩衝區
           [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      //掃描記憶集的時間
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      //掃描程式碼中的Root(區域性變數)節點時間
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
      //拷貝(疏散)物件的時間
      [Object Copy (ms): Min: 3.0, Avg: 3.0, Max: 3.1, Diff: 0.0, Sum: 12.1]
      //執行緒竊取演算法,每個執行緒完成任務後會嘗試幫其他執行緒完成剩餘的任務
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
            //執行緒成功竊取任務的次數
			[Termination Attempts: Min: 1, Avg: 1.3, Max: 2, Diff: 1, Sum: 5]
      //GC 過程中完成其他任務的時間
      [GC Worker Other (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 0.8]
      //展示每個垃圾收集執行緒的最小、最大、平均、差值和總共時間。
      [GC Worker Total (ms): Min: 4.2, Avg: 4.3, Max: 4.3, Diff: 0.1, Sum: 17.1]
      //min表示最早的完成任務的執行緒的時間,max表示最晚接受任務的執行緒時間
      [GC Worker End (ms): Min: 1050.6, Avg: 1050.6, Max: 1050.6, Diff: 0.0]
   //釋放管理並行垃圾收集活動資料結構
   [Code Root Fixup: 0.0 ms]
   //清理其他資料結構
   [Code Root Purge: 0.0 ms]
   //清理card table (Remember Set)
   [Clear CT: 0.8 ms]
   //其他功能
   [Other: 7.8 ms]
      //評估需要收集的區域。YongGC 並不是全部收集,而是根據期望收集
      [Choose CSet: 0.0 ms]
      //處理Java中各種引用soft、weak、final、phantom、JNI等等。
      [Ref Proc: 5.2 ms]
      //遍歷所有的引用,將不能回收的放入pending列表
      [Ref Enq: 0.1 ms]
      //在回收過程中被修改的card將會被重置為dirty
      [Redirty Cards: 0.4 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      //將要釋放的分割槽還回到free列表。
      [Free CSet: 0.1 ms]
   //Eden 回收前使用3072K 總共12M ——> 回收後使用0B,總共11M
   //Survivors 回收前使用0B,回收後使用1024K
   //整個堆回收前使用101M 總共256M,回收後使用99M,總共256M
   //可以看到這裡新生代沒有佔滿就開始Yong GC,其目的是為了開啟Concurrent Marking
   [Eden: 3072.0K(12.0M)->0.0B(11.0M) Survivors: 0.0B->1024.0K Heap: 101.0M(256.0M)->99.0M(256.0M)]
 [Times: user=0.00 sys=0.00, real=0.01 secs] 

Concurrent Marking

併發標記一般發生在Yong GC之後。Yong GC之後便完成initial mark

//掃描GC Roots
[GC concurrent-root-region-scan-start]
//掃描GC Roots完成,花費0.0004341s
[GC concurrent-root-region-scan-end, 0.0004341 secs]
//併發標記階段開始
[GC concurrent-mark-start]
//併發標記介紹。花費0.0002240s
[GC concurrent-mark-end, 0.0002240 secs]
//重新標記開始,會STW.
//Finalize Marking花費0.0006341s
//處理引用:主要是若引用。花費0.0000478 secs
//解除安裝類。花費0.0008091 secs
//總共花費0.0020776 secs
[GC remark [Finalize Marking, 0.0006341 secs] [GC ref-proc, 0.0000478 secs] [Unloading, 0.0008091 secs], 0.0020776 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs]
//清理階段 會STW
//主要: 標記所有`initial mark`階段之後分配的物件,標記至少有一個存活物件的Region
//清理沒有存活物件的Old Region和Humongous Region
//處理沒有任何存活物件的RSet
//對所有Old Region 按照物件的存活率進行排序
//清理Humongous Region前使用了150M,清理後使用了150M ,耗時0.0013110s
[GC cleanup 150M->150M(256M), 0.0013110 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

Mixed GC

當併發標記完成後,就可以進行Mixed GC了,Mixed GC主要工作就是回收併發標記過程中篩選出來的Region

Mixed GC做的工作和Yong GC流程基本一樣,只不過回收的內容是依據併發標記而來的。

G1可能不能一口氣將所有的候選分割槽收集掉,因此G1可能會產生連續多次的混合收集與應用執行緒交替執行

  [GC pause (G1 Evacuation Pause) (mixed), 0.0080519 secs]
   [Parallel Time: 7.6 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 140411.4, Avg: 140415.1, Max: 140418.9, Diff: 7.4]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.5, Diff: 0.4, Sum: 1.0]
      [Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
         [Processed Buffers: Min: 0, Avg: 1.3, Max: 4, Diff: 4, Sum: 5]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [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: 2.6, Max: 5.2, Diff: 5.2, Sum: 10.3]
      [Termination (ms): Min: 0.0, Avg: 0.9, Max: 1.8, Diff: 1.8, Sum: 3.4]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 0.1, Avg: 3.8, Max: 7.5, Diff: 7.4, Sum: 15.2]
      [GC Worker End (ms): Min: 140418.9, Avg: 140418.9, Max: 140418.9, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.4 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 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]
   [Eden: 4096.0K(4096.0K)->0.0B(138.0M) Survivors: 8192.0K->2048.0K Heap: 68.0M(256.0M)->65.5M(256.0M)]
 [Times: user=0.03 sys=0.00, real=0.01 secs] 

這裡貼出日誌,只是為了說明他們內容是一樣的,因此這裡就不再繼續註釋。


Full GC

G1 Full GC

//GC型別:Full GC  GC原因:呼叫System.gc() GC前記憶體使用298M GC後使用509K 花費時間:0.0101774s 
[Full GC (System.gc()) 298M->509K(512M), 0.0101774 secs]
  //新生代:GC前使用122M GC後使用0B 總容量由154M擴容為230M
  //倖存區:GC前使用4096K GC後使用0B 
  //總記憶體:GC前使用298M GC後使用509K 總量量512M不變
  //元空間:GC前使用3308K GC後使用3308K 總容量1056768K
  [Eden: 122.0M(154.0M)->0.0B(230.0M) Survivors: 4096.0K->0.0B Heap: 298.8M(512.0M)->509.4K(512.0M)], [Metaspace: 3308K->3308K(1056768K)]

[Times: user=0.01 sys=0.00, real=0.01 secs]

可以看到這次GC使用的時間是10ms,但是這僅僅是在整個堆只有512M,且只使用300M的情況下,G1是沒有Full GC的機制的,G1 GC是使用的Serial Old的程式碼(後面被優化為多執行緒,但是速度相對來說依然比較慢),因此Full GC會暫停很久,因此在生產環境中,一定注意Full GC,正常來說幾天一次Full GC是可以接受的。

G1 Full GC的原因一般有:

  • Mixed GC趕不上記憶體分配的速度,只能通過Full GC來釋放記憶體,這種情況解決方案後面再說

  • MetaSpace不足,對於大量使用反射,動態代理的類,由於動態代理的每個類都會生成一個新的類,同時class資訊會存放在元空間,因此如果元空間不足,G1會靠Full GC來擴容元空間,這種情況解決方案就是擴大初始元空間大小。

  • Humongous分配失敗,前面說過G1分配大物件時,回收是靠Concurrent MarkingFull GC,因此如果大物件分配失敗,則可能會引發Full GC

    具體規則這裡不太明白,因為測試的時候,Humongous都是引發的Concurrent Marking


G1調優

前面說了這麼多,就是為了明白當GC影響到線上環境的時候的時候,應該怎麼去調整。因此,明白了G1的回收過程,就能大體的明白每個引數的作用,應該如何去修改。

  • -XX:+UseG1GC : 使用G1回收器。

  • -XX:MaxGCPauseMillis = 200 :設定最大暫停目標。預設為200,G1只會僅最大努力達到這個目標,這個目標值需要結合專案進行調整,時間太短,則可能會引起吞吐下降,同時每次Mixed GC回收的垃圾過少,導致最後垃圾堆積引起Full GC,時間太長,則可能會引起使用者體驗不佳。

  • -XX:InitiatingHeapOccupancyPercent = 45 : 表示併發開始GC週期的堆使用閾值,當整個堆的使用量達到閾值時,就會開始併發週期。這個引數和CMS一樣主要用來防止Mixed GC 過程中的併發失敗,如果過晚進行併發回收,則可能會因為併發過程中剩餘的記憶體不足以滿足使用者所樹妖的記憶體,這就會導致G1放棄併發標記,升級為Full GC.這種情況一般都能在GC中看到to-space exhausted字樣。

    這個引數也不能調的太小,太小會導致一直迴圈,佔用CPU資源。

  • -XX:G1MixedGCCountTarget=8 : 每次Mixed GC回收的老年代記憶體數量,預設為8,這也是為了解決to-space exhausted的問題,每次Mixed GC多回收一些,老年代空餘的記憶體就會多一些,但是相應的可能會導致暫停時間增加

  • -XX:ConcGCThreads : 每次GC使用的CPU數量, 值不是固定。同樣也是為了解決to-space exhausted的問題,使用執行緒多,則GC便會快一些,代價是使用者的CPU時間會被佔用。

  • -XX:G1ReservePercent=10%: 假天花板數量,作用是預留10%的空間不使用,留給併發週期過程中當可能會出現to-space exhausted的問題時候使用,防止出現to-space exhausted,預留過多可能會導致記憶體浪費

  • 不要設定年輕代大小:不要使用-Xmn,因為G1是通過需要擴充套件或縮小年輕代大小,如果設定了年輕代大小,則會導致G1無法使用暫停時間目標。


以上,只是G1的常見需要注意的引數,當然還可能有其他問題,比如大物件的分配,元空間大小等等, 總體來說,明白GC的回收過程,多多實踐,大體就能通過GC的日誌找出問題所在。

胖毛說,總結經典Java書籍讀書筆記

參考文獻:

Java Hotspot G1 GC的一些關鍵技術 -- 美團技術團隊

請教G1演算法的原理

深入理解G1的GC日誌(一)

Garbage First Garbage Collector Tuning

HotSpot虛擬機器垃圾收集優化指南--G1

相關文章