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停頓周期;防止高并發下應用雪崩現象
g1在運作過程中主要包含如下4種操作方式:
ygc(不同于cms)
并發階段
混合模式
full gc (一般是g1出現問題時發生)
下面是一次ygc前後記憶體區域是示意圖:
圖中每個小區塊都代表g1的一個區域(region),區塊裡面的字母代表不同的分代記憶體空間類型(如[e]eden,[o]old,[s]survivor)空白的區塊不屬于任何一個分區;g1可以在需要的時候任意指定這個區域屬于eden或是o區之類的。
g1 younggc在eden充滿時觸發,在回收之後所有之前屬于eden的區塊全變成空白。然後至少有一個區塊是屬于s區的(如圖半滿的那個區域),同時可能有一些資料移到了o區。
目前淘系的應用大都使用printgcdetails參數打出gc日志、這個參數對g1同樣有效、但日志内容頗為不同;下面是一個young gc的例子:
<code>23.430: [gc pause (young), 0.23094400 secs]</code>
<code>...</code>
<code>[eden: 1286m(1286m)->0b(1212m)</code>
<code>survivors: 78m->152m heap: 1454m(4096m)->242m(4096m)]</code>
<code>[times: user=0.85 sys=0.05, real=0.23 secs]</code>
上面日志的内容解析: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、下面是一個日志示例:
<code>50.541: [gc pause (young) (initial-mark), 0.27767100 secs]</code>
<code>[eden: 1220m(1220m)->0b(1220m)</code>
<code>survivors: 144m->144m heap: 3242m(4096m)->2093m(4096m)]</code>
<code>[times: user=1.02 sys=0.04, real=0.28 secs]</code>
上面的日志表明發生了ygc、應用線程為此暫停了280毫秒,eden區被清空(71mb從young區移到了o區)。
日志裡面initial-mark的字樣表明背景的并發gc階段開始了。因為初始标記階段本身也是要暫停應用線程的,
g1正好在ygc的過程中把這個事情也一起幹了。為此帶來的額外開銷不是很大、增加了20%的cpu,暫停時間相應的略微變長了些。
接下來,g1開始掃描根區域、日志示例:
<code>50.819: [gc concurrent-root-region-scan-start]</code>
<code>51.408: [gc concurrent-root-region-scan-end, 0.5890230]</code>
一共花了580毫秒,這個過程沒有暫停應用線程;是背景線程并行處理的。這個階段不能被ygc所打斷、是以背景線程有足夠的cpu時間很關鍵。如果young區空間恰好在root掃描的時候
滿了、ygc必須等待root掃描之後才能進行。帶來的影響是ygc暫停時間會相應的增加。這時的gc日志是這樣的:
<code>350.994: [gc pause (young)</code>
<code>351.093: [gc concurrent-root-region-scan-end, 0.6100090]</code>
<code>351.093: [gc concurrent-mark-start],0.37559600 secs]</code>
gc暫停這裡可以看出在root掃描結束之前就發生了,表明ygc發生了等待,等待時間大概是100毫秒。
在root掃描完成後,g1進入了一個并發标記階段。這個階段也是完全背景進行的;gc日志裡面下面的資訊代表這個階段的開始和結束:
<code>111.382: [gc concurrent-mark-start]</code>
<code>....</code>
<code>120.905: [gc concurrent-mark-end, 9.5225160 sec]</code>
并發标記階段是可以被打斷的,比如這個過程中發生了ygc就會。這個階段之後會有一個二次标記階段和清理階段:
<code>120.910: [gc remark 120.959:</code>
<code>[gc ref-prc, 0.0000890 secs], 0.0718990 secs]</code>
<code>[times: user=0.23 sys=0.01, real=0.08 secs]</code>
<code>120.985: [gc cleanup 3510m->3434m(4096m), 0.0111040 secs]</code>
<code>[times: user=0.04 sys=0.00, real=0.01 secs]</code>
這兩個階段同樣會暫停應用線程,但時間很短。接下來還有額外的一次并發清理階段:
<code>120.996: [gc concurrent-cleanup-start]</code>
<code>120.996: [gc concurrent-cleanup-end, 0.0004520]</code>
到此為止,正常的一個g1周期已完成–這個周期主要做的是發現哪些區域包含可回收的垃圾最多(标記為x),實際空間釋放較少。
接下來g1執行一系列的混合gc。這個時期因為會同時進行ygc和清理上面已标記為x的區域,是以稱之為混合階段,下面是一個混合gc執行的前後示意圖:
像普通的ygc那樣、g1完全清空掉eden同時調整survivor區。另外,兩個标記也被回收了,他們有個共同的特點是包含最多可回收的對象,是以這兩個區域絕對部分空間都被釋放了。這兩個區域任何存活的對象都被移到了其他區域(和ygc存活對象晉升到o區類似)。這就是為什麼g1的堆比cms記憶體碎片要少很多的原因–移動這些對象的同時也就是在壓縮對記憶體。下面是一個混合gc的日志:
<code>79.826: [gc pause (mixed), 0.26161600 secs]</code>
<code>[eden: 1222m(1222m)->0b(1220m)</code>
<code>survivors: 142m->144m heap: 3200m(4096m)->1964m(4096m)]</code>
<code>[times: user=1.01 sys=0.00, real=0.26 secs]</code>
上面的日志可以注意到eden釋放了1222mb、但整個堆的空間釋放記憶體要大于這個數目。數量相差看起來比較少、隻有16mb,但是要考慮同時有survivor區的對象晉升到o區;另外,每次混合gc隻是清理一部分的o區記憶體,整個gc會一直持續到幾乎所有的标記區域垃圾對象都被回收,這個階段完了之後g1會重新回到正常的ygc階段。周期性的,當o區記憶體占用達到一定數量之後g1又會開啟一次新的并行gc階段.