總結《深入理解JVM》 G1 篇


注:一下內容主要結合《深入理解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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM