深入理解G1垃圾收集器
G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作為JVM GC選項;作為JVM GC算法的一次重大升級、DK7u後G1已相對穩定、且未來計劃替代CMS、所以有必要深入了解下:
不同於其他的分代回收算法、G1將堆空間劃分成了互相獨立的區塊。每塊區域既有可能屬於O區、也有可能是Y區,且每類區域空間可以是不連續的(對比CMS的O區和Y區都必須是連續的)。這種將O區劃分成多塊的理念源於:當並發後台線程尋找可回收的對象時、有些區塊包含可回收的對象要比其他區塊多很多。雖然在清理這些區塊時G1仍然需要暫停應用線程、但可以用相對較少的時間優先回收包含垃圾最多區塊。這也是為什麼G1命名為Garbage First的原因:第一時間處理垃圾最多的區塊。
平時工作中大多數係統都使用CMS、即使靜默升級到JDK7默認仍然采用CMS、那麼G1相對於CMS的區別在:
- G1在壓縮空間方麵有優勢
- G1通過將內存空間分成區域(Region)的方式避免內存碎片問題
- Eden, Survivor, Old區不再固定、在內存使用效率上來說更靈活
- G1可以通過設置預期停頓時間(Pause Time)來控製垃圾收集時間避免應用雪崩現象
- G1在回收內存後會馬上同時做合並空閑內存的工作、而CMS默認是在STW(stop the world)的時候做
- G1會在Young GC中使用、而CMS隻能在O區使用
就目前而言、CMS還是默認首選的GC策略、可能在以下場景下G1更適合:
- 服務端多核CPU、JVM內存占用較大的應用(至少大於4G)
- 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
- 想要更可控、可預期的GC停頓周期;防止高並發下應用雪崩現象
一次完整G1GC的詳細過程:
G1在運行過程中主要包含如下4種操作方式:
- YGC(不同於CMS)
- 並發階段
- 混合模式
- full GC (一般是G1出現問題時發生)
YGC:
下麵是一次YGC前後內存區域是示意圖:
圖中每個小區塊都代表G1的一個區域(Region),區塊裏麵的字母代表不同的分代內存空間類型(如[E]Eden,[O]Old,[S]Survivor)空白的區塊不屬於任何一個分區;G1可以在需要的時候任意指定這個區域屬於Eden或是O區之類的。
G1 YoungGC在Eden充滿時觸發,在回收之後所有之前屬於Eden的區塊全變成空白。然後至少有一個區塊是屬於S區的(如圖半滿的那個區域),同時可能有一些數據移到了O區。
目前淘係的應用大都使用PrintGCDetails參數打出GC日誌、這個參數對G1同樣有效、但日誌內容頗為不同;下麵是一個Young GC的例子:
23.430: [GC pause (young), 0.23094400 secs]
...
[Eden: 1286M(1286M)->0B(1212M)
Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)]
[Times: user=0.85 sys=0.05, real=0.23 secs]
上麵日誌的內容解析:Young GC實際占用230毫秒、其中GC線程占用850毫秒的CPU時間
E:內存占用從1286MB變成0、都被移出
S:從78M增長到了152M、說明從Eden移過來74M
Heap:占用從1454變成242M、說明這次Young GC一共釋放了1212M內存空間
很多情況下,S區的對象會有部分晉升到Old區,另外如果S區已滿、Eden存活的對象會直接晉升到Old區,這種情況下Old的空間就會漲
並發階段:
一個並發G1回收周期前後內存占用情況如下圖所示:
從上麵的圖表可以看出以下幾點:
1、Young區發生了變化、這意味著在G1並發階段內至少發生了一次YGC(這點和CMS就有區別),Eden在標記之前已經被完全清空,因為在並發階段應用線程同時在工作、所以可以看到Eden又有新的占用
2、一些區域被X標記,這些區域屬於O區,此時仍然有數據存放、不同之處在G1已標記出這些區域包含的垃圾最多、也就是回收收益最高的區域
3、在並發階段完成之後實際上O區的容量變得更大了(O+X的方塊)。這時因為這個過程中發生了YGC有新的對象進入所致。此外,這個階段在O區沒有回收任何對象:它的作用主要是標記出垃圾最多的區塊出來。對象實際上是在後麵的階段真正開始被回收
G1並發標記周期可以分成幾個階段、其中有些需要暫停應用線程。第一個階段是初始標記階段。這個階段會暫停所有應用線程-部分原因是這個過程會執行一次YGC、下麵是一個日誌示例:
50.541: [GC pause (young) (initial-mark), 0.27767100 secs]
[Eden: 1220M(1220M)->0B(1220M)
Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)]
[Times: user=1.02 sys=0.04, real=0.28 secs]
上麵的日誌表明發生了YGC、應用線程為此暫停了280毫秒,Eden區被清空(71MB從Young區移到了O區)。
日誌裏麵initial-mark的字樣表明後台的並發GC階段開始了。因為初始標記階段本身也是要暫停應用線程的,
G1正好在YGC的過程中把這個事情也一起幹了。為此帶來的額外開銷不是很大、增加了20%的CPU,暫停時間相應的略微變長了些。
接下來,G1開始掃描根區域、日誌示例:
50.819: [GC concurrent-root-region-scan-start]
51.408: [GC concurrent-root-region-scan-end, 0.5890230]
一共花了580毫秒,這個過程沒有暫停應用線程;是後台線程並行處理的。這個階段不能被YGC所打斷、因此後台線程有足夠的CPU時間很關鍵。如果Young區空間恰好在Root掃描的時候
滿了、YGC必須等待root掃描之後才能進行。帶來的影響是YGC暫停時間會相應的增加。這時的GC日誌是這樣的:
350.994: [GC pause (young)
351.093: [GC concurrent-root-region-scan-end, 0.6100090]
351.093: [GC concurrent-mark-start],0.37559600 secs]
GC暫停這裏可以看出在root掃描結束之前就發生了,表明YGC發生了等待,等待時間大概是100毫秒。
在root掃描完成後,G1進入了一個並發標記階段。這個階段也是完全後台進行的;GC日誌裏麵下麵的信息代表這個階段的開始和結束:
111.382: [GC concurrent-mark-start]
....
120.905: [GC concurrent-mark-end, 9.5225160 sec]
並發標記階段是可以被打斷的,比如這個過程中發生了YGC就會。這個階段之後會有一個二次標記階段和清理階段:
120.910: [GC remark 120.959:
[GC ref-PRC, 0.0000890 secs], 0.0718990 secs]
[Times: user=0.23 sys=0.01, real=0.08 secs]
120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs]
[Times: user=0.04 sys=0.00, real=0.01 secs]
這兩個階段同樣會暫停應用線程,但時間很短。接下來還有額外的一次並發清理階段:
120.996: [GC concurrent-cleanup-start]
120.996: [GC concurrent-cleanup-end, 0.0004520]
到此為止,正常的一個G1周期已完成–這個周期主要做的是發現哪些區域包含可回收的垃圾最多(標記為X),實際空間釋放較少。
混合GC:
接下來G1執行一係列的混合GC。這個時期因為會同時進行YGC和清理上麵已標記為X的區域,所以稱之為混合階段,下麵是一個混合GC執行的前後示意圖:
像普通的YGC那樣、G1完全清空掉Eden同時調整survivor區。另外,兩個標記也被回收了,他們有個共同的特點是包含最多可回收的對象,因此這兩個區域絕對部分空間都被釋放了。這兩個區域任何存活的對象都被移到了其他區域(和YGC存活對象晉升到O區類似)。這就是為什麼G1的堆比CMS內存碎片要少很多的原因–移動這些對象的同時也就是在壓縮對內存。下麵是一個混合GC的日誌:
79.826: [GC pause (mixed), 0.26161600 secs]
....
[Eden: 1222M(1222M)->0B(1220M)
Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)]
[Times: user=1.01 sys=0.00, real=0.26 secs]
上麵的日誌可以注意到Eden釋放了1222MB、但整個堆的空間釋放內存要大於這個數目。數量相差看起來比較少、隻有16MB,但是要考慮同時有survivor區的對象晉升到O區;另外,每次混合GC隻是清理一部分的O區內存,整個GC會一直持續到幾乎所有的標記區域垃圾對象都被回收,這個階段完了之後G1會重新回到正常的YGC階段。周期性的,當O區內存占用達到一定數量之後G1又會開啟一次新的並行GC階段.
最後更新:2017-05-22 20:04:33