1. 程式人生 > >深入理解G1垃圾收集器

深入理解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的區別在:

  1. G1在壓縮空間方面有優勢
  2. G1通過將記憶體空間分成區域(Region)的方式避免記憶體碎片問題
  3. Eden, Survivor, Old區不再固定、在記憶體使用效率上來說更靈活
  4. G1可以通過設定預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
  5. G1在回收記憶體後會馬上同時做合併空閒記憶體的工作、而CMS預設是在STW(stop the world)的時候做
  6. G1會在Young GC中使用、而CMS只能在O區使用

就目前而言、CMS還是預設首選的GC策略、可能在以下場景下G1更適合:

  1. 服務端多核CPU、JVM記憶體佔用較大的應用(至少大於4G)
  2. 應用在執行過程中會產生大量記憶體碎片、需要經常壓縮空間
  3. 想要更可控、可預期的GC停頓週期;防止高併發下應用雪崩現象

一次完整G1GC的詳細過程:

G1在執行過程中主要包含如下4種操作方式:

  1. YGC(不同於CMS)
  2. 併發階段
  3. 混合模式
  4. full GC (一般是G1出現問題時發生)

YGC:

下面是一次YGC前後記憶體區域是示意圖:

G1-1

G1-2

圖中每個小區塊都代表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回收週期前後記憶體佔用情況如下圖所示:

G1-3

G1-4

從上面的圖表可以看出以下幾點:
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執行的前後示意圖:

G1-5

G1-6

像普通的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階段.