G1從入門到放棄(一)

G1從入門到放棄(一)

最近在看關(guān)于G1垃圾收集的文章,看了很多國內(nèi)與國外的資料,本文對G1的這些資料進(jìn)行了整理。這篇合適JVM垃圾回收有一定基礎(chǔ)的同學(xué),作為G1入門可以看一下,如果要死磕G1實(shí)現(xiàn)的內(nèi)容細(xì)節(jié)。大家可以找R大。 個人認(rèn)為R大是目前國內(nèi)JVM領(lǐng)域研究的先驅(qū)了,當(dāng)然R大也是不建議大家去看JVM的源碼的。為啥別讀HotSpot VM的源碼
G1系列第一篇文章會介紹G1的理論知識,不會做JVM源碼的深入分析。第二篇準(zhǔn)備介紹G1實(shí)踐中的日志分析。

為什么要學(xué)G1

G1(Garbadge First Collector)作為一款JVM最新的垃圾收集器,可以解決CMS中Concurrent Mode Failed問題,盡量縮短處理超大堆的停頓,在G1進(jìn)行垃圾回收的時候完成內(nèi)存壓縮,降低內(nèi)存碎片的生成。G1在堆內(nèi)存比較大的時候表現(xiàn)出比較高吞吐量和短暫的停頓時間,而且已成為Java 9的默認(rèn)收集器。未來替代CMS只是時間的問題。

G1的GC原理

Region

G1的內(nèi)存結(jié)構(gòu)和傳統(tǒng)的內(nèi)存空間劃分有比較的不同。G1將內(nèi)存劃分成了多個大小相等的Region(默認(rèn)是512K),Region邏輯上連續(xù),物理內(nèi)存地址不連續(xù)。同時每個Region被標(biāo)記成E、S、O、H,分別表示Eden、Survivor、Old、Humongous。其中E、S屬于年輕代,O與H屬于老年代。
示意圖如下:


image.png

H表示Humongous。從字面上就可以理解表示大的對象(下面簡稱H對象)。當(dāng)分配的對象大于等于Region大小的一半的時候就會被認(rèn)為是巨型對象。H對象默認(rèn)分配在老年代,可以防止GC的時候大對象的內(nèi)存拷貝。通過如果發(fā)現(xiàn)堆內(nèi)存容不下H對象的時候,會觸發(fā)一次GC操作。

跨代引用

在進(jìn)行Young GC的時候,Young區(qū)的對象可能還存在Old區(qū)的引用, 這就是跨代引用的問題。為了解決Young GC的時候,掃描整個老年代,G1引入了Card TableRemember Set的概念,基本思想就是用空間換時間。這兩個數(shù)據(jù)結(jié)構(gòu)是專門用來處理Old區(qū)到Y(jié)oung區(qū)的引用。Young區(qū)到Old區(qū)的引用則不需要單獨(dú)處理,因?yàn)閅oung區(qū)中的對象本身變化比較大,沒必要浪費(fèi)空間去記錄下來。

  • RSet:全稱Remembered Sets, 用來記錄外部指向本Region的所有引用,每個Region維護(hù)一個RSet。
  • Card: JVM將內(nèi)存劃分成了固定大小的Card。這里可以類比物理內(nèi)存上page的概念。

下圖展示的是RSetCard的關(guān)系。每個Region被分成了多個Card,其中綠色部分的Card表示該Card中有對象引用了其他Card中的對象,這種引用關(guān)系用藍(lán)色實(shí)線表示。RSet其實(shí)是一個HashTable,Key是Region的起始地址,Value是Card Table (字節(jié)數(shù)組),字節(jié)數(shù)組下標(biāo)表示Card的空間地址,當(dāng)該地址空間被引用的時候會被標(biāo)記為dirty_card。

image.png

關(guān)于RSet結(jié)構(gòu)的維護(hù),可以參考這篇文章,這里不做過多的深入。

SATB

SATB的全稱(Snapshot At The Beginning)字面意思是開始GC前存活對象的一個快照。SATB的作用是保證在并發(fā)標(biāo)記階段的正確性。如何理解這句話?
首先要介紹三色標(biāo)記算法。


image.png
  • 黑色:根對象,或者該對象與它的子對象都被掃描
  • 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
  • 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達(dá)對象,即垃圾對象。

在GC掃描C之前的顏色如下:


image.png

在并發(fā)標(biāo)記階段,應(yīng)用線程改變了這種引用關(guān)系

A.c=C
B.c=null

得到如下結(jié)果。


image.png

在重新標(biāo)記階段掃描結(jié)果如下


image.png

這種情況下C會被當(dāng)做垃圾進(jìn)行回收。Snapshot的存活對象原來是A、B、C,現(xiàn)在變成A、B了,Snapshot的完整遭到破壞了,顯然這個做法是不合理。
G1采用的是pre-write barrier解決這個問題。簡單說就是在并發(fā)標(biāo)記階段,當(dāng)引用關(guān)系發(fā)生變化的時候,通過pre-write barrier函數(shù)會把這種這種變化記錄并保存在一個隊(duì)列里,在JVM源碼中這個隊(duì)列叫satb_mark_queue。在remark階段會掃描這個隊(duì)列,通過這種方式,舊的引用所指向的對象就會被標(biāo)記上,其子孫也會被遞歸標(biāo)記上,這樣就不會漏標(biāo)記任何對象,snapshot的完整性也就得到了保證。

這里引用R大對SATB的解釋:

其實(shí)只需要用pre-write barrier把每次引用關(guān)系變化時舊的引用值記下來就好了。這樣,等concurrent marker到達(dá)某個對象時,這個對象的所有引用類型字段的變化全都有記錄在案,就不會漏掉任何在snapshot里活的對象。當(dāng)然,很可能有對象在snapshot中是活的,但隨著并發(fā)GC的進(jìn)行它可能本來已經(jīng)死了,但SATB還是會讓它活過這次GC。CMS的incremental update設(shè)計使得它在remark階段必須重新掃描所有線程棧和整個young gen作為root;G1的SATB設(shè)計在remark階段則只需要掃描剩下的satb_mark_queue ,解決了CMS垃圾收集器重新標(biāo)記階段長時間STW的潛在風(fēng)險。"

SATB的方式記錄活對象,也就是那一時刻對象snapshot, 但是在之后這里面的對象可能會變成垃圾, 叫做浮動垃圾(floating garbage),這種對象只能等到下一次收集回收掉。在GC過程中新分配的對象都當(dāng)做是活的,其他不可達(dá)的對象就是死的。
如何知道哪些對象是GC開始之后新分配的呢?
在Region中通過top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS來記錄新配的對象。示意圖如下:

image.png

每個region記錄著兩個top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。 這里引用R大的解釋。

G1的concurrent marking用了兩個bitmap: 一個prevBitmap記錄第n-1輪concurrent marking所得的對象存活狀態(tài)。由于第n-1輪concurrent marking已經(jīng)完成,這個bitmap的信息可以直接使用。 一個nextBitmap記錄第n輪concurrent marking的結(jié)果。這個bitmap是當(dāng)前將要或正在進(jìn)行的concurrent marking的結(jié)果,尚未完成,所以還不能使用。

其中top是該region的當(dāng)前分配指針,[bottom, top)是當(dāng)前該region已用(used)的部分,[top, end)是尚未使用的可分配空間(unused)。
(1): [bottom, prevTAMS): 這部分里的對象存活信息可以通過prevBitmap來得知
(2): [prevTAMS, nextTAMS): 這部分里的對象在第n-1輪concurrent marking是隱式存活的
(3): [nextTAMS, top): 這部分里的對象在第n輪concurrent marking是隱式存活的

G1的GC模式

Young GC

Young GC 回收的是所有年輕代的Region。當(dāng)E區(qū)不能再分配新的對象時就會觸發(fā)。E區(qū)的對象會移動到S區(qū),當(dāng)S區(qū)空間不夠的時候,E區(qū)的對象會直接晉升到O區(qū),同時S區(qū)的數(shù)據(jù)移動到新的S區(qū),如果S區(qū)的部分對象到達(dá)一定年齡,會晉升到O區(qū)。
Yung GC過程示意圖如下:

image.png

Mixed GC

Mixed GC 翻譯過來叫混合回收。之所以叫混合是因?yàn)榛厥账械哪贻p代的Region+部分老年代的Region。
1、為什么是老年代的部分Region?
2、什么時候觸發(fā)Mixed GC?
這兩個問題其實(shí)可以一并回答?;厥?strong>部分老年代是參數(shù)-XX:MaxGCPauseMillis,用來指定一個G1收集過程目標(biāo)停頓時間,默認(rèn)值200ms,當(dāng)然這只是一個期望值。G1的強(qiáng)大之處在于他有一個停頓預(yù)測模型(Pause Prediction Model),他會有選擇的挑選部分Region,去盡量滿足停頓時間,關(guān)于G1的這個模型是如何建立的,這里不做深究。
Mixed GC的觸發(fā)也是由一些參數(shù)控制。比如XX:InitiatingHeapOccupancyPercent表示老年代占整個堆大小的百分比,默認(rèn)值是45%,達(dá)到該閾值就會觸發(fā)一次Mixed GC。

Mixed GC主要可以分為兩個階段:
1、全局并發(fā)標(biāo)記(global concurrent marking)
全局并發(fā)標(biāo)記又可以進(jìn)一步細(xì)分成下面幾個步驟:

  • 初始標(biāo)記(initial mark,STW)。它標(biāo)記了從GC Root開始直接可達(dá)的對象。初始標(biāo)記階段借用young GC的暫停,因而沒有額外的、單獨(dú)的暫停階段。
  • 并發(fā)標(biāo)記(Concurrent Marking)。這個階段從GC Root開始對heap中的對象標(biāo)記,標(biāo)記線程與應(yīng)用程序線程并行執(zhí)行,并且收集各個Region的存活對象信息。過程中還會掃描上文中提到的SATB write barrier所記錄下的引用。
  • 最終標(biāo)記(Remark,STW)。標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對象,將被回收。
  • 清除垃圾(Cleanup,部分STW)。這個階段如果發(fā)現(xiàn)完全沒有活對象的region就會將其整體回收到可分配region列表中。 清除空Region。

2、拷貝存活對象(Evacuation)
Evacuation階段是全暫停的。它負(fù)責(zé)把一部分region里的活對象拷貝到空region里去(并行拷貝),然后回收原本的region的空間。Evacuation階段可以自由選擇任意多個region來獨(dú)立收集構(gòu)成收集集合(collection set,簡稱CSet),CSet集合中Region的選定依賴于上文中提到的停頓預(yù)測模型,該階段并不evacuate所有有活對象的region,只選擇收益高的少量region來evacuate,這種暫停的開銷就可以(在一定范圍內(nèi))可控。

Mixed GC的清理過程示意圖如下:


image.png

Full GC

G1的垃圾回收過程是和應(yīng)用程序并發(fā)執(zhí)行的,當(dāng)Mixed GC的速度趕不上應(yīng)用程序申請內(nèi)存的速度的時候,Mixed G1就會降級到Full GC,使用的是Serial GC。Full GC會導(dǎo)致長時間的STW,應(yīng)該要盡量避免。
導(dǎo)致G1 Full GC的原因可能有兩個:

  1. Evacuation的時候沒有足夠的to-space來存放晉升的對象;
  2. 并發(fā)處理過程完成之前空間耗盡

PS: 本文主要參考的國內(nèi)文章:
java Hotspot G1 GC的一些關(guān)鍵技術(shù)
Garbage First G1收集器 理解和原理分析
G1: One Garbage Collector To Rule Them All
請教G1算法的原理
深入理解 Java G1 垃圾收集器
Getting Started with the G1 Garbage Collector!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容