G1 面向服務(wù)端(多CPU)應(yīng)用的垃圾回收器
總則:首先收集盡可能多的垃圾(Garbage First)
一定程度上,可以理解為 是CMS在全局不分區(qū)的一種改進(jìn)。G1并不會(huì)等內(nèi)存耗盡(串行、并行)或者快耗盡(CMS)的時(shí)候開始垃圾收集,而是在內(nèi)部采用了啟發(fā)式算法,在老年代找出具有高收集收益的分區(qū)進(jìn)行收集。
特點(diǎn):
并發(fā)與并行:G1能充分的利用多CPU,多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU來縮短STW停頓時(shí)間。部分收集器需要停頓Java線程來執(zhí)行GC動(dòng)作,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)運(yùn)行。
分代收集:G1可以獨(dú)自管理整個(gè)Java堆,只進(jìn)行邏輯上的年輕代與老年代的區(qū)別。采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間的對(duì)象和已經(jīng)熬過多次GC的舊對(duì)象以獲得更好的收集效果。
空間整合:G1運(yùn)作期間不會(huì)產(chǎn)生空間鎖片,收集后能提供規(guī)整可用的內(nèi)存。G 1將內(nèi)存劃分為一個(gè)個(gè)相等大小的內(nèi)存分區(qū),回收時(shí)則以分區(qū)為單位進(jìn)行回收,存活的對(duì)象復(fù)制到另一個(gè)空閑分區(qū)中。由于都是以相等大小的分區(qū)為單位進(jìn)行操作,因此G1天然就是一種壓縮方案(局部壓縮);
可預(yù)測(cè)的停頓:G1除了追求低停頓外,還能建立可以預(yù)測(cè)停頓時(shí)間的模型。能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間段,消耗在垃圾回收上的時(shí)間不超過N毫秒??梢愿鶕?jù)用戶設(shè)置的暫停時(shí)間目標(biāo)自動(dòng)調(diào)整年輕代和總堆大小,暫停目標(biāo)越短年輕代空間越小、總空間就越大;
G1模型
內(nèi)存模型

分區(qū)(Region)
G1采用了內(nèi)存分區(qū)的概念,將整堆分為若干大小相等的區(qū)域逐步使用;G1僅要求對(duì)象邏輯上連續(xù)。區(qū)域也不會(huì)跟代進(jìn)行綁定,可以切換代所屬??梢酝ㄟ^ -XX:G1HeapRegionSize=n設(shè)置整堆的大小(1-32mb,2^n),默認(rèn)將整堆劃分為2048個(gè)分區(qū)。說白了就是-Xms /2048 ,如果這個(gè)值小于1,則取值為1,大于32則取值32;其它值則取與2,4,8,16相近的。
卡片(card)
每個(gè)分組內(nèi)部劃分多個(gè)大小為512byte的卡片。同時(shí)G1 GC為每個(gè)區(qū)間設(shè)置了一個(gè)全局內(nèi)存塊表(Global Card Table)來幫助維護(hù)所有的堆內(nèi)存塊。內(nèi)存回收是對(duì)卡片進(jìn)行處理的。
堆 (head)
代表整體空間總大小。可用-Xms/-Xmx來指定。在發(fā)生年輕代收集或混合收集的時(shí)候,會(huì)通過計(jì)算GC與應(yīng)用的耗費(fèi)時(shí)間比,自動(dòng)調(diào)整堆空間。GC頻率高則增大堆空間,GC占用時(shí)間高則減小堆空間。GC時(shí)間與應(yīng)用耗時(shí)比默認(rèn)為9??臻g不足時(shí)會(huì)先嘗試擴(kuò)容,失敗則進(jìn)行Full GC。
分代模型

分代的分割
更關(guān)心最近被分配的對(duì)象,避免對(duì)長(zhǎng)生命周期的對(duì)象進(jìn)行改動(dòng)。借鑒了分代思想,將內(nèi)存區(qū)邏輯區(qū)分為年輕代、幸存代、老年代。但是JVM會(huì)動(dòng)態(tài)的調(diào)整空閑區(qū)間到年輕代空間。
年輕代會(huì)在初始空間(-XX:G1NewSizePercent 默認(rèn) 5%)到最大空間(-XX:G1MaxNewSizePercent默認(rèn)60%) 之間動(dòng)態(tài)變化,且由參數(shù)目標(biāo)暫停時(shí)間(-XX:MaxGCPauseMillis默認(rèn)200ms)
本地分配緩沖 Local allocation buffer (Lab)
由于是分區(qū)的內(nèi)存,所以可以每個(gè)線程領(lǐng)取一部分內(nèi)存使用。這樣領(lǐng)取后的內(nèi)存與GC所進(jìn)行任務(wù)的內(nèi)存都是獨(dú)立進(jìn)行的,從而減少同步時(shí)間,提高GC時(shí)的效率。稱這種線程領(lǐng)取后的分區(qū)稱之為本地分配緩存。
分區(qū)模型

G1對(duì)內(nèi)存的分配是以“分區(qū)(Region)”為單位,針對(duì)區(qū)內(nèi)對(duì)“對(duì)象”的分配是以“卡片(Card)”為單位。
巨型對(duì)象區(qū)域 (Humongous Region)
大于了分區(qū)大小一半以上的對(duì)象成為巨型對(duì)象。由于巨型對(duì)象的移動(dòng)成本很高,甚至分區(qū)無法容納對(duì)象,所以線程并不會(huì)直接在TLAB中創(chuàng)建對(duì)象。針對(duì)巨型對(duì)象,會(huì)直接分配在老年代的連續(xù)空間中,所占用的連續(xù)空間叫做“巨型分區(qū)(Humongous Region)”。優(yōu)化: 巨型對(duì)象如果沒有指向巨型對(duì)象,直接會(huì)在年輕代收集周期中被回收。
巨型對(duì)象會(huì)獨(dú)占一個(gè)、或多個(gè)物理連續(xù)分區(qū),其中第一個(gè)分區(qū)被標(biāo)記為開始巨型(StartsHumongous),相鄰連續(xù)分區(qū)被標(biāo)記為連續(xù)巨型(ContinuesHumongous)。由于無法享受Lab帶來的優(yōu)化,并且確定一片連續(xù)的內(nèi)存空間需要掃描整堆,因此確定巨型對(duì)象開始位置的成本非常高,如果可以,應(yīng)用程序應(yīng)避免生成巨型對(duì)象。
已記憶集合 Remember Set (RSet)
Serial 和Parllel GC的時(shí)候是掃描整堆來確認(rèn)可達(dá)性。G1 通過為每個(gè)分區(qū)建立一個(gè)已記憶集合 (RSet)記錄引用分區(qū)內(nèi)對(duì)象的卡片索引(反向索引,誰引用了我),當(dāng)要回收該分區(qū)時(shí),通過掃描分區(qū)的RSet,來確定引用本分區(qū)內(nèi)的對(duì)象是否存活,進(jìn)而確定本分區(qū)內(nèi)的對(duì)象存活情況。
RSet空了,說明這個(gè)Region中的中的對(duì)象已經(jīng)沒有被其他對(duì)象引用了。
兩種場(chǎng)景下依賴Rset加速(由于年輕代會(huì)被完整回收,同時(shí)因?yàn)閷懫琳闲阅芟模圆挥涗浤贻p代飲用)。
- 老年代引用老年代對(duì)象,Rset保存在老年代中
- 老年代引用年輕代對(duì)象,Rset保存在年輕代中
Per Region Table (PRT)
RSet通過PRT記錄分區(qū)的引用情況。當(dāng)一個(gè)指針引用到Rset中的一個(gè)區(qū)間時(shí)(圖右上角),包含該指針的堆塊就會(huì)被PRT標(biāo)記。PRT需要內(nèi)存空間來存儲(chǔ)這些引用關(guān)系,根據(jù)引用的數(shù)量,PRT有三種的記錄引用的模式,會(huì)根據(jù)調(diào)用次數(shù)轉(zhuǎn)變: 稀疏(hash)->細(xì)粒度->粗粒度。
稀疏表:通過哈希表來存儲(chǔ),key是region index,value是card數(shù)組
細(xì)粒度PerRegionTable:當(dāng)稀疏表指定region的card數(shù)量超過閾值時(shí),則在細(xì)粒度PRT中創(chuàng)建一個(gè)對(duì)應(yīng)的PerRegionTable對(duì)象。一個(gè)Region地址鏈表,維護(hù)當(dāng)前Region中所有card對(duì)應(yīng)的一個(gè)BitMap集合。
粗粒度位圖:當(dāng)細(xì)粒度PRT size超過閾值時(shí),所有region 形成一個(gè) bitMap。如果有region 對(duì)當(dāng)前 Region 有指針指向,就設(shè)置其對(duì)應(yīng)的bit 為1
CSet

收集集合(CSet)代表每次GC暫停時(shí)回收的一系列目標(biāo)分區(qū)。在收集暫停中,CSet Region都會(huì)被釋放,存活的對(duì)象會(huì)被分配到空閑分區(qū)中。G1的收集都是根據(jù)CSet進(jìn)行操作的,年輕代收集與混合收集沒有明顯的不同,最大的區(qū)別在于兩種收集的觸發(fā)條件。
年輕代收集集合:
當(dāng)年輕代空間增長(zhǎng)到Eden已經(jīng)滿了的時(shí)候,便會(huì)觸發(fā)一次STW式的年輕代收集。Eden分區(qū)存活的對(duì)象將被拷貝到Survivor分區(qū);原有Survivor分區(qū)存活的對(duì)象,將根據(jù)任期閾值(tenuring threshold)分別晉升到PLAB中、新的survivor分區(qū)和老年代分區(qū)。而原有的年輕代分區(qū)將被整體回收掉。
混合收集集合:
老年代的空間被逐漸填充。當(dāng)老年代占用空間超過整堆比IHOP閾值
-XX:InitiatingHeapOccupancyPercent(默認(rèn)45%)時(shí),G1就會(huì)啟動(dòng)一次混合垃圾收集周期。為了減小暫停目標(biāo),混合收集會(huì)分批次,與用戶線程交替執(zhí)行,每次STW的混合收集與年輕代收集過程相類似。
G1的活動(dòng)周期
工作流程:

RSet的維護(hù)
G1通過維護(hù)RSet來記錄對(duì)象分區(qū)之間的引用,避免全量掃堆。維護(hù)RSet通過兩個(gè)方面:
寫屏障(Write Barrier)
并發(fā)優(yōu)化線程(Concurrence Refinement Threads)
寫屏障

屏障指在原生代碼片段中,當(dāng)某些語句被執(zhí)行時(shí),柵欄代碼也會(huì)被執(zhí)行(類似aop)。G1主要在賦值語句中,使用寫前柵欄(Pre-Write Barrrier)和寫后柵欄(Post-Write Barrrier)。注意: 寫柵欄的指令序列開銷非常昂貴,應(yīng)用吞吐量也會(huì)根據(jù)柵欄復(fù)雜度而降低。
寫前屏障: 在進(jìn)行等式賦值前,等式左側(cè)原先的引用就會(huì)失去一個(gè)引用,所以jvm就需要在語句執(zhí)行前記錄喪失的引用對(duì)象。(并不是立即執(zhí)行,是后續(xù)批量執(zhí)行,使用SATB方法)
寫后屏障: 賦值等式執(zhí)行后,右側(cè)的引用多了一個(gè)引用,需要確實(shí)否是需要增加引用,JVM也不會(huì)立即處理,會(huì)先進(jìn)行記錄,然后會(huì)在后續(xù)進(jìn)行批量處理(并發(fā)優(yōu)化線程)。
SATB + RSet 解決了什么問題?
主要是為了解決并發(fā)標(biāo)記過程中,出現(xiàn)的漏標(biāo),誤標(biāo)等問題。
起始快照算法(Snapshot at the beginning (SATB))-寫前屏障處理
G1 SATB和Incremental Update算法的理解
SATB是一種增量式完全并發(fā)標(biāo)記算法,針對(duì)并發(fā)標(biāo)記階段,適用于G1的分塊堆結(jié)構(gòu)。

SATB 算法認(rèn)為開始標(biāo)記的都認(rèn)為是活的對(duì)象,SATB會(huì)創(chuàng)建一個(gè)對(duì)象圖,相當(dāng)于堆的邏輯快照。所以對(duì)象可按圖結(jié)構(gòu)遍歷(用RSet可以加速)。當(dāng)發(fā)生已掃描對(duì)象引用未掃描對(duì)象時(shí)(黑色引用白色),通過write barrier寫屏障技術(shù),會(huì)把B到D 的引用推到gc 遍歷執(zhí)行的堆棧上,這樣在后續(xù)會(huì)繼續(xù)針對(duì)D進(jìn)行掃描。
Snapshot at the beginning,可以理解為,在進(jìn)行掃描之前,將整個(gè)對(duì)象引用關(guān)系都認(rèn)為是 存活的節(jié)點(diǎn)進(jìn)行掃描,如果在掃描過程中把B = null,這是D其實(shí)已經(jīng)是垃圾了,但是在后續(xù)流程中仍然會(huì)處理D并標(biāo)記為黑色。D在本輪本該清除,但是會(huì)暫時(shí)保留,在remark階段處理。
每個(gè)線程有一個(gè)256條記錄的SATB緩存區(qū),當(dāng)滿了之后會(huì)將數(shù)據(jù)加入到全局列表中中并重新申請(qǐng)新的一批256緩存。還會(huì)定期檢查和處理全局緩沖區(qū)列表的記錄,然后根據(jù)標(biāo)記位圖分片的標(biāo)記位,掃描引用字段來更新RSet。此過程又稱為并發(fā)標(biāo)記/SATB寫前柵欄。
并發(fā)優(yōu)化線程(Concurrence Refinement Threads)- 寫后屏障處理
寫后屏障(需要2個(gè)額外指令)會(huì)更新一個(gè)Card Table Type結(jié)構(gòu)來跟蹤代間引用。觸發(fā)寫屏障時(shí),會(huì)過濾是否為本分區(qū)的對(duì)象,如果發(fā)生跨區(qū)應(yīng)用更新,則將更新對(duì)象的卡片加入到緩沖(新日志緩沖或者臟卡片)。一旦日志緩沖區(qū)用盡,則分配一個(gè)新的日志緩沖區(qū),并將原來的緩沖區(qū)加入全局列表中。
并發(fā)優(yōu)化線程會(huì)一直活躍,當(dāng)全局列表中有數(shù)據(jù)就會(huì)處理來更新RSet。如果處理不過來,就會(huì)讓更多線程來處理全局表;如果還處理不過來,會(huì)暫停應(yīng)用線程來處理全局列表。(對(duì)象變更過于頻繁)
并發(fā)標(biāo)記周期
并發(fā)標(biāo)記周期(Concurrent Marking Cycle)需要會(huì)為混合收集周期識(shí)別垃圾最多的老年代分區(qū)。整個(gè)周期完成根標(biāo)記、識(shí)別可能存活對(duì)象、計(jì)算分區(qū)活躍從而確定GC效率等級(jí)。
觸發(fā)條件: 達(dá)到IHOP閾值(
-XX:InitiatingHeapOccupancyPercent 老年代整堆比,默認(rèn)45%),
步驟(STW):
- 初始標(biāo)記(Initial Mark)
- 根分區(qū)掃描(Root Region Scanning)
- 并發(fā)標(biāo)記(Concurrent Marking)
- 重新標(biāo)記(Remark)
- 清除(Cleanup)
- 并發(fā)標(biāo)記線程 (Concurrent Marking Threads)

并發(fā)標(biāo)記階段,會(huì)針對(duì)分區(qū)進(jìn)行數(shù)據(jù)標(biāo)記,一個(gè)分區(qū)會(huì)會(huì)建立兩個(gè)位圖來記錄標(biāo)記結(jié)果:Previous Bitmap、Next Bitmap。
- Previous Bitmap為上一次的標(biāo)記(已經(jīng)完成標(biāo)記)
- Next Bitmap為本次的標(biāo)記結(jié)果(即將進(jìn)行標(biāo)記)
對(duì)應(yīng)的,針對(duì)標(biāo)記的位置利用兩個(gè)變量記錄(TAMS: Top-At-Mark_Start)分別是:
- Previous TAMS(PTAMS) 前一次
- Next TAMS(NTAMS) 后一次
總的來說: 區(qū)間內(nèi)的數(shù)據(jù)用兩個(gè)Bitmap兩個(gè)‘指針’進(jìn)行滾動(dòng)回收,當(dāng)回收完畢后,區(qū)間回收。
初始標(biāo)記結(jié)束后:會(huì)先將Next Bitmap設(shè)置為空,并將N TAMS設(shè)置到區(qū)間頂部。此時(shí)P TAMS仍然為上一大輪時(shí)標(biāo)記的位置。
并發(fā)循環(huán): 會(huì)對(duì) PTAMS 到 NTAMS中間的數(shù)據(jù)進(jìn)行標(biāo)記,將本段標(biāo)記的結(jié)果+上一次標(biāo)記的結(jié)果一同更新到Next Bitmap。
一次循環(huán)結(jié)束: 將Next Bitmap 更新到 Previous Bitmap中,同時(shí),更新 PTAMS與 NTAMS的位置。
在并發(fā)標(biāo)記階段,G1根據(jù)參數(shù)(-XX:ConcGCThreads(默認(rèn)GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4))分配線程,每個(gè)線程一次流程只進(jìn)行一個(gè)分區(qū)的掃描。
并發(fā)標(biāo)記循環(huán)流程 (Concurrent Marking Cycle)
并發(fā)循環(huán)的流程為:初始標(biāo)記、根分區(qū)掃描、并發(fā)標(biāo)記、重新標(biāo)記、清除階段。
初始標(biāo)記(Initial Mark):
獨(dú)占,會(huì)觸發(fā)STW,然后標(biāo)記GC ROOT可達(dá)的所有對(duì)象。
當(dāng)IHOP觸發(fā)閾值的時(shí)候,G1并不會(huì)立即開啟循環(huán),而是等待下一次年輕代收集(同樣需要STW),和年輕代收集一起在一次STW之間執(zhí)行(借道(Piggybacking))。
在初始標(biāo)記暫停中,分區(qū)的NTAMS都被設(shè)置到分區(qū)頂部Top,初始標(biāo)記是并發(fā)執(zhí)行,會(huì)處理所有的分區(qū)。
根分區(qū)掃描(Root Region Scanning)
當(dāng)STW結(jié)束,年輕代收集和初始標(biāo)記完成后?;跇?biāo)記算法,拷貝到幸存者區(qū)間的區(qū)間也需要被當(dāng)作根元素進(jìn)行標(biāo)記,同時(shí)G1會(huì)掃描這個(gè)區(qū)間,然后將幸存者區(qū)間所引用的對(duì)象進(jìn)行標(biāo)記。所以幸存者區(qū)間也被稱為根區(qū)間。
特別的: 因?yàn)?并發(fā)循環(huán)流程中會(huì)多次執(zhí)行年輕代收集(被年輕代收集打斷),所以需要在下一次被打斷前完成。
并發(fā)標(biāo)記(Concurrent Marking)
該階段和應(yīng)用縣城并發(fā)執(zhí)行,每個(gè)線程一次只掃描一個(gè)分區(qū),標(biāo)記出存活對(duì)象圖。在這一階段會(huì)處理Previous/Next標(biāo)記位圖,掃描標(biāo)記對(duì)象的引用字段。同時(shí)并發(fā)標(biāo)記線程還會(huì)處理SATB的全局列表信息。
重新標(biāo)記(Remark)
最后一個(gè)標(biāo)記動(dòng)作,會(huì)是一個(gè)獨(dú)占(STW)階段,可以并行執(zhí)行。在整個(gè)階段中,會(huì)處理所有遺留的SATB日志。找出所有未被訪問的存活對(duì)象。
注意:引用處理也是重新標(biāo)記階段的一部分,所有重度使用引用對(duì)象(弱引用、軟引用、虛引用、最終引用)的應(yīng)用都會(huì)在引用處理上產(chǎn)生開銷。
清理(Cleanup)
清理結(jié)算會(huì)識(shí)別并清理完全空閑的區(qū)域(RSet中引用計(jì)數(shù)空了)并清理空閑的區(qū)域。這個(gè)階段會(huì)處理Previous/Next標(biāo)記位圖、以及PTAMS/NTAMS。主要進(jìn)行的操作有一下幾種:
RSet梳理(根據(jù)掃描后的結(jié)果,確認(rèn)RSet各個(gè)粒度中的數(shù)據(jù)是否維護(hù)的正確)
整理堆分區(qū),為分區(qū)確定訪問熱度,為后續(xù)確認(rèn)回收收益高的分區(qū)。
識(shí)別空閑區(qū)間直接回收。
年輕代收集/混合收集周期
年輕代收集(Young Collection)
年輕代由:Eden 和 Survivor組成,當(dāng)Eden分配失敗的時(shí)候會(huì)觸發(fā),執(zhí)行年輕代收集的時(shí)候會(huì)進(jìn)行中斷(STW)
對(duì)象復(fù)制 Object Copy: 根據(jù)CSet 會(huì)將Eden中的存活對(duì)象復(fù)制到Survivor區(qū)間。注意:會(huì)將所有的年輕代對(duì)象(Eden和Survivor)拷貝到新的Survivor區(qū)間。
對(duì)象提升: 如果對(duì)象經(jīng)歷的幸存次數(shù)達(dá)到閾值,會(huì)提升到老年代中,這個(gè)閾值經(jīng)過計(jì)算配置得到。
分區(qū)調(diào)整: 收集期間,G1會(huì)計(jì)算當(dāng)前年輕代需要擴(kuò)容或者壓縮的總量例如:
- 空閑區(qū)間
- RSet大小
- 當(dāng)前最大可用年輕代
- 當(dāng)前最小可用年輕代
- 停頓時(shí)間
信息維護(hù): 記錄收集對(duì)象的年齡信息"Age Info",便于后續(xù)是否晉升到老年區(qū)、時(shí)候在混合收集階段進(jìn)行回收。
更新RSet:會(huì)處理還沒有提交到全局列表中的本地緩沖區(qū)中的RSet更新日志。
掃描RSet: 在收集當(dāng)前CSet之前,考慮到分區(qū)外的引用,必須掃描CSet分區(qū)的RSet。
釋放分區(qū): 釋放Free CSet
混合收集周期(Mixed Collection Cycle)
當(dāng)一次并發(fā)標(biāo)記循環(huán)完成了之后,會(huì)開始一次混合收集周期,在周期中G1不僅收集年輕代,也同時(shí)收集老年代,而收集那些老年代,則是根據(jù)老年代中的垃圾數(shù)量確定的。
單輪的混合收集與年輕代收集無區(qū)別,但是因?yàn)槔夏甏赡鼙容^多,為了滿足暫停的需求,可能會(huì)連續(xù)的進(jìn)行多次的混合收集。再過程中,G1會(huì)計(jì)算下一次處理的CSet集合的分區(qū)數(shù)量,是否本次收集之后需要結(jié)束周期。
Full GC
當(dāng)無法申請(qǐng)新的空間時(shí),會(huì)執(zhí)行一次STW式的、單線程的Full GC。Full GC會(huì)對(duì)整堆做標(biāo)記清除和壓縮,最后將只包含純粹的存活對(duì)象。觸發(fā)Full GC的方式:
- 從年輕代分區(qū)拷貝存活對(duì)象時(shí),無法找到可用的空閑分區(qū)
- 從老年代分區(qū)轉(zhuǎn)移存活對(duì)象時(shí),無法找到可用的空閑分區(qū)
- 分配巨型對(duì)象時(shí)在老年代無法找到足夠的連續(xù)分區(qū)
流程簡(jiǎn)述:

最后
垃圾回收器可以說事Java的基石之一,垃圾回收器的實(shí)現(xiàn)充滿了大量的實(shí)現(xiàn)細(xì)節(jié),對(duì)于一些優(yōu)化十分具有參考價(jià)值。



























