G1 面向服務(wù)端(多CPU)應(yīng)用的垃圾回收器
總則:首先收集盡可能多的垃圾(Garbage First)
一定程度上,可以理解為 是CMS在全局不分區(qū)的一種改進(jìn)。G1并不會等內(nèi)存耗盡(串行、并行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內(nèi)部采用了啟發(fā)式算法,在老年代找出具有高收集收益的分區(qū)進(jìn)行收集。
特點:
并發(fā)與并行:G1能充分的利用多CPU,多核環(huán)境下的硬件優(yōu)勢,使用多個CPU來縮短STW停頓時間。部分收集器需要停頓Java線程來執(zhí)行GC動作,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)運行。
分代收集:G1可以獨自管理整個Java堆,只進(jìn)行邏輯上的年輕代與老年代的區(qū)別。采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時間的對象和已經(jīng)熬過多次GC的舊對象以獲得更好的收集效果。
空間整合:G1運作期間不會產(chǎn)生空間鎖片,收集后能提供規(guī)整可用的內(nèi)存。G 1將內(nèi)存劃分為一個個相等大小的內(nèi)存分區(qū),回收時則以分區(qū)為單位進(jìn)行回收,存活的對象復(fù)制到另一個空閑分區(qū)中。由于都是以相等大小的分區(qū)為單位進(jìn)行操作,因此G1天然就是一種壓縮方案(局部壓縮);
可預(yù)測的停頓:G1除了追求低停頓外,還能建立可以預(yù)測停頓時間的模型。能讓使用者明確指定在一個長度為M毫秒的時間段,消耗在垃圾回收上的時間不超過N毫秒??梢愿鶕?jù)用戶設(shè)置的暫停時間目標(biāo)自動調(diào)整年輕代和總堆大小,暫停目標(biāo)越短年輕代空間越小、總空間就越大;
G1模型
內(nèi)存模型
分區(qū)(Region)
G1采用了內(nèi)存分區(qū)的概念,將整堆分為若干大小相等的區(qū)域逐步使用;G1僅要求對象邏輯上連續(xù)。區(qū)域也不會跟代進(jìn)行綁定,可以切換代所屬。可以通過 -XX:G1HeapRegionSize=n設(shè)置整堆的大小(1-32mb,2^n),默認(rèn)將整堆劃分為2048個分區(qū)。說白了就是-Xms /2048 ,如果這個值小于1,則取值為1,大于32則取值32;其它值則取與2,4,8,16相近的。
卡片(card)
每個分組內(nèi)部劃分多個大小為512byte的卡片。同時G1 GC為每個區(qū)間設(shè)置了一個全局內(nèi)存塊表(Global Card Table)來幫助維護(hù)所有的堆內(nèi)存塊。內(nèi)存回收是對卡片進(jìn)行處理的。
堆 (head)
代表整體空間總大小??捎?Xms/-Xmx來指定。在發(fā)生年輕代收集或混合收集的時候,會通過計算GC與應(yīng)用的耗費時間比,自動調(diào)整堆空間。GC頻率高則增大堆空間,GC占用時間高則減小堆空間。GC時間與應(yīng)用耗時比默認(rèn)為9??臻g不足時會先嘗試擴容,失敗則進(jìn)行Full GC。
分代模型
分代的分割
更關(guān)心最近被分配的對象,避免對長生命周期的對象進(jìn)行改動。借鑒了分代思想,將內(nèi)存區(qū)邏輯區(qū)分為年輕代、幸存代、老年代。但是JVM會動態(tài)的調(diào)整空閑區(qū)間到年輕代空間。
年輕代會在初始空間(-XX:G1NewSizePercent 默認(rèn) 5%)到最大空間(-XX:G1MaxNewSizePercent默認(rèn)60%) 之間動態(tài)變化,且由參數(shù)目標(biāo)暫停時間(-XX:MaxGCPauseMillis默認(rèn)200ms)
本地分配緩沖 Local allocation buffer (Lab)
由于是分區(qū)的內(nèi)存,所以可以每個線程領(lǐng)取一部分內(nèi)存使用。這樣領(lǐng)取后的內(nèi)存與GC所進(jìn)行任務(wù)的內(nèi)存都是獨立進(jìn)行的,從而減少同步時間,提高GC時的效率。稱這種線程領(lǐng)取后的分區(qū)稱之為本地分配緩存。
分區(qū)模型
G1對內(nèi)存的分配是以“分區(qū)(Region)”為單位,針對區(qū)內(nèi)對“對象”的分配是以“卡片(Card)”為單位。
巨型對象區(qū)域 (Humongous Region)
大于了分區(qū)大小一半以上的對象成為巨型對象。由于巨型對象的移動成本很高,甚至分區(qū)無法容納對象,所以線程并不會直接在TLAB中創(chuàng)建對象。針對巨型對象,會直接分配在老年代的連續(xù)空間中,所占用的連續(xù)空間叫做“巨型分區(qū)(Humongous Region)”。優(yōu)化: 巨型對象如果沒有指向巨型對象,直接會在年輕代收集周期中被回收。
巨型對象會獨占一個、或多個物理連續(xù)分區(qū),其中第一個分區(qū)被標(biāo)記為開始巨型(StartsHumongous),相鄰連續(xù)分區(qū)被標(biāo)記為連續(xù)巨型(ContinuesHumongous)。由于無法享受Lab帶來的優(yōu)化,并且確定一片連續(xù)的內(nèi)存空間需要掃描整堆,因此確定巨型對象開始位置的成本非常高,如果可以,應(yīng)用程序應(yīng)避免生成巨型對象。
已記憶集合 Remember Set (RSet)
Serial 和Parllel GC的時候是掃描整堆來確認(rèn)可達(dá)性。G1 通過為每個分區(qū)建立一個已記憶集合 (RSet)記錄引用分區(qū)內(nèi)對象的卡片索引(反向索引,誰引用了我),當(dāng)要回收該分區(qū)時,通過掃描分區(qū)的RSet,來確定引用本分區(qū)內(nèi)的對象是否存活,進(jìn)而確定本分區(qū)內(nèi)的對象存活情況。
RSet空了,說明這個Region中的中的對象已經(jīng)沒有被其他對象引用了。
兩種場景下依賴Rset加速(由于年輕代會被完整回收,同時因為寫屏障性能消耗,所以不記錄年輕代飲用)。
- 老年代引用老年代對象,Rset保存在老年代中
- 老年代引用年輕代對象,Rset保存在年輕代中
Per Region Table (PRT)
RSet通過PRT記錄分區(qū)的引用情況。當(dāng)一個指針引用到Rset中的一個區(qū)間時(圖右上角),包含該指針的堆塊就會被PRT標(biāo)記。PRT需要內(nèi)存空間來存儲這些引用關(guān)系,根據(jù)引用的數(shù)量,PRT有三種的記錄引用的模式,會根據(jù)調(diào)用次數(shù)轉(zhuǎn)變: 稀疏(hash)->細(xì)粒度->粗粒度。
稀疏表:通過哈希表來存儲,key是region index,value是card數(shù)組
細(xì)粒度PerRegionTable:當(dāng)稀疏表指定region的card數(shù)量超過閾值時,則在細(xì)粒度PRT中創(chuàng)建一個對應(yīng)的PerRegionTable對象。一個Region地址鏈表,維護(hù)當(dāng)前Region中所有card對應(yīng)的一個BitMap集合。
粗粒度位圖:當(dāng)細(xì)粒度PRT size超過閾值時,所有region 形成一個 bitMap。如果有region 對當(dāng)前 Region 有指針指向,就設(shè)置其對應(yīng)的bit 為1
CSet
收集集合(CSet)代表每次GC暫停時回收的一系列目標(biāo)分區(qū)。在收集暫停中,CSet Region都會被釋放,存活的對象會被分配到空閑分區(qū)中。G1的收集都是根據(jù)CSet進(jìn)行操作的,年輕代收集與混合收集沒有明顯的不同,最大的區(qū)別在于兩種收集的觸發(fā)條件。
年輕代收集集合:
當(dāng)年輕代空間增長到Eden已經(jīng)滿了的時候,便會觸發(fā)一次STW式的年輕代收集。Eden分區(qū)存活的對象將被拷貝到Survivor分區(qū);原有Survivor分區(qū)存活的對象,將根據(jù)任期閾值(tenuring threshold)分別晉升到PLAB中、新的survivor分區(qū)和老年代分區(qū)。而原有的年輕代分區(qū)將被整體回收掉。
混合收集集合:
老年代的空間被逐漸填充。當(dāng)老年代占用空間超過整堆比IHOP閾值
-XX:InitiatingHeapOccupancyPercent(默認(rèn)45%)時,G1就會啟動一次混合垃圾收集周期。為了減小暫停目標(biāo),混合收集會分批次,與用戶線程交替執(zhí)行,每次STW的混合收集與年輕代收集過程相類似。
G1的活動周期
工作流程:
RSet的維護(hù)
G1通過維護(hù)RSet來記錄對象分區(qū)之間的引用,避免全量掃堆。維護(hù)RSet通過兩個方面:
寫屏障(Write Barrier)
并發(fā)優(yōu)化線程(Concurrence Refinement Threads)
寫屏障
屏障指在原生代碼片段中,當(dāng)某些語句被執(zhí)行時,柵欄代碼也會被執(zhí)行(類似aop)。G1主要在賦值語句中,使用寫前柵欄(Pre-Write Barrrier)和寫后柵欄(Post-Write Barrrier)。注意: 寫柵欄的指令序列開銷非常昂貴,應(yīng)用吞吐量也會根據(jù)柵欄復(fù)雜度而降低。
寫前屏障: 在進(jìn)行等式賦值前,等式左側(cè)原先的引用就會失去一個引用,所以jvm就需要在語句執(zhí)行前記錄喪失的引用對象。(并不是立即執(zhí)行,是后續(xù)批量執(zhí)行,使用SATB方法)
寫后屏障: 賦值等式執(zhí)行后,右側(cè)的引用多了一個引用,需要確實否是需要增加引用,JVM也不會立即處理,會先進(jìn)行記錄,然后會在后續(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)記算法,針對并發(fā)標(biāo)記階段,適用于G1的分塊堆結(jié)構(gòu)。
SATB 算法認(rèn)為開始標(biāo)記的都認(rèn)為是活的對象,SATB會創(chuàng)建一個對象圖,相當(dāng)于堆的邏輯快照。所以對象可按圖結(jié)構(gòu)遍歷(用RSet可以加速)。當(dāng)發(fā)生已掃描對象引用未掃描對象時(黑色引用白色),通過write barrier寫屏障技術(shù),會把B到D 的引用推到gc 遍歷執(zhí)行的堆棧上,這樣在后續(xù)會繼續(xù)針對D進(jìn)行掃描。
Snapshot at the beginning,可以理解為,在進(jìn)行掃描之前,將整個對象引用關(guān)系都認(rèn)為是 存活的節(jié)點進(jìn)行掃描,如果在掃描過程中把B = null,這是D其實已經(jīng)是垃圾了,但是在后續(xù)流程中仍然會處理D并標(biāo)記為黑色。D在本輪本該清除,但是會暫時保留,在remark階段處理。
每個線程有一個256條記錄的SATB緩存區(qū),當(dāng)滿了之后會將數(shù)據(jù)加入到全局列表中中并重新申請新的一批256緩存。還會定期檢查和處理全局緩沖區(qū)列表的記錄,然后根據(jù)標(biāo)記位圖分片的標(biāo)記位,掃描引用字段來更新RSet。此過程又稱為并發(fā)標(biāo)記/SATB寫前柵欄。
并發(fā)優(yōu)化線程(Concurrence Refinement Threads)- 寫后屏障處理
寫后屏障(需要2個額外指令)會更新一個Card Table Type結(jié)構(gòu)來跟蹤代間引用。觸發(fā)寫屏障時,會過濾是否為本分區(qū)的對象,如果發(fā)生跨區(qū)應(yīng)用更新,則將更新對象的卡片加入到緩沖(新日志緩沖或者臟卡片)。一旦日志緩沖區(qū)用盡,則分配一個新的日志緩沖區(qū),并將原來的緩沖區(qū)加入全局列表中。
并發(fā)優(yōu)化線程會一直活躍,當(dāng)全局列表中有數(shù)據(jù)就會處理來更新RSet。如果處理不過來,就會讓更多線程來處理全局表;如果還處理不過來,會暫停應(yīng)用線程來處理全局列表。(對象變更過于頻繁)
并發(fā)標(biāo)記周期
并發(fā)標(biāo)記周期(Concurrent Marking Cycle)需要會為混合收集周期識別垃圾最多的老年代分區(qū)。整個周期完成根標(biāo)記、識別可能存活對象、計算分區(qū)活躍從而確定GC效率等級。
觸發(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)記階段,會針對分區(qū)進(jìn)行數(shù)據(jù)標(biāo)記,一個分區(qū)會會建立兩個位圖來記錄標(biāo)記結(jié)果:Previous Bitmap、Next Bitmap。
- Previous Bitmap為上一次的標(biāo)記(已經(jīng)完成標(biāo)記)
- Next Bitmap為本次的標(biāo)記結(jié)果(即將進(jìn)行標(biāo)記)
對應(yīng)的,針對標(biāo)記的位置利用兩個變量記錄(TAMS: Top-At-Mark_Start)分別是:
- Previous TAMS(PTAMS) 前一次
- Next TAMS(NTAMS) 后一次
總的來說: 區(qū)間內(nèi)的數(shù)據(jù)用兩個Bitmap兩個‘指針’進(jìn)行滾動回收,當(dāng)回收完畢后,區(qū)間回收。
初始標(biāo)記結(jié)束后:會先將Next Bitmap設(shè)置為空,并將N TAMS設(shè)置到區(qū)間頂部。此時P TAMS仍然為上一大輪時標(biāo)記的位置。
并發(fā)循環(huán): 會對 PTAMS 到 NTAMS中間的數(shù)據(jù)進(jìn)行標(biāo)記,將本段標(biāo)記的結(jié)果+上一次標(biāo)記的結(jié)果一同更新到Next Bitmap。
一次循環(huán)結(jié)束: 將Next Bitmap 更新到 Previous Bitmap中,同時,更新 PTAMS與 NTAMS的位置。
在并發(fā)標(biāo)記階段,G1根據(jù)參數(shù)(-XX:ConcGCThreads(默認(rèn)GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4))分配線程,每個線程一次流程只進(jìn)行一個分區(qū)的掃描。
并發(fā)標(biāo)記循環(huán)流程 (Concurrent Marking Cycle)
并發(fā)循環(huán)的流程為:初始標(biāo)記、根分區(qū)掃描、并發(fā)標(biāo)記、重新標(biāo)記、清除階段。
初始標(biāo)記(Initial Mark):
獨占,會觸發(fā)STW,然后標(biāo)記GC ROOT可達(dá)的所有對象。
當(dāng)IHOP觸發(fā)閾值的時候,G1并不會立即開啟循環(huán),而是等待下一次年輕代收集(同樣需要STW),和年輕代收集一起在一次STW之間執(zhí)行(借道(Piggybacking))。
在初始標(biāo)記暫停中,分區(qū)的NTAMS都被設(shè)置到分區(qū)頂部Top,初始標(biāo)記是并發(fā)執(zhí)行,會處理所有的分區(qū)。
根分區(qū)掃描(Root Region Scanning)
當(dāng)STW結(jié)束,年輕代收集和初始標(biāo)記完成后?;跇?biāo)記算法,拷貝到幸存者區(qū)間的區(qū)間也需要被當(dāng)作根元素進(jìn)行標(biāo)記,同時G1會掃描這個區(qū)間,然后將幸存者區(qū)間所引用的對象進(jìn)行標(biāo)記。所以幸存者區(qū)間也被稱為根區(qū)間。
特別的: 因為 并發(fā)循環(huán)流程中會多次執(zhí)行年輕代收集(被年輕代收集打斷),所以需要在下一次被打斷前完成。
并發(fā)標(biāo)記(Concurrent Marking)
該階段和應(yīng)用縣城并發(fā)執(zhí)行,每個線程一次只掃描一個分區(qū),標(biāo)記出存活對象圖。在這一階段會處理Previous/Next標(biāo)記位圖,掃描標(biāo)記對象的引用字段。同時并發(fā)標(biāo)記線程還會處理SATB的全局列表信息。
重新標(biāo)記(Remark)
最后一個標(biāo)記動作,會是一個獨占(STW)階段,可以并行執(zhí)行。在整個階段中,會處理所有遺留的SATB日志。找出所有未被訪問的存活對象。
注意:引用處理也是重新標(biāo)記階段的一部分,所有重度使用引用對象(弱引用、軟引用、虛引用、最終引用)的應(yīng)用都會在引用處理上產(chǎn)生開銷。
清理(Cleanup)
清理結(jié)算會識別并清理完全空閑的區(qū)域(RSet中引用計數(shù)空了)并清理空閑的區(qū)域。這個階段會處理Previous/Next標(biāo)記位圖、以及PTAMS/NTAMS。主要進(jìn)行的操作有一下幾種:
RSet梳理(根據(jù)掃描后的結(jié)果,確認(rèn)RSet各個粒度中的數(shù)據(jù)是否維護(hù)的正確)
整理堆分區(qū),為分區(qū)確定訪問熱度,為后續(xù)確認(rèn)回收收益高的分區(qū)。
識別空閑區(qū)間直接回收。
年輕代收集/混合收集周期
年輕代收集(Young Collection)
年輕代由:Eden 和 Survivor組成,當(dāng)Eden分配失敗的時候會觸發(fā),執(zhí)行年輕代收集的時候會進(jìn)行中斷(STW)
對象復(fù)制 Object Copy: 根據(jù)CSet 會將Eden中的存活對象復(fù)制到Survivor區(qū)間。注意:會將所有的年輕代對象(Eden和Survivor)拷貝到新的Survivor區(qū)間。
對象提升: 如果對象經(jīng)歷的幸存次數(shù)達(dá)到閾值,會提升到老年代中,這個閾值經(jīng)過計算配置得到。
分區(qū)調(diào)整: 收集期間,G1會計算當(dāng)前年輕代需要擴容或者壓縮的總量例如:
- 空閑區(qū)間
- RSet大小
- 當(dāng)前最大可用年輕代
- 當(dāng)前最小可用年輕代
- 停頓時間
信息維護(hù): 記錄收集對象的年齡信息"Age Info",便于后續(xù)是否晉升到老年區(qū)、時候在混合收集階段進(jìn)行回收。
更新RSet:會處理還沒有提交到全局列表中的本地緩沖區(qū)中的RSet更新日志。
掃描RSet: 在收集當(dāng)前CSet之前,考慮到分區(qū)外的引用,必須掃描CSet分區(qū)的RSet。
釋放分區(qū): 釋放Free CSet
混合收集周期(Mixed Collection Cycle)
當(dāng)一次并發(fā)標(biāo)記循環(huán)完成了之后,會開始一次混合收集周期,在周期中G1不僅收集年輕代,也同時收集老年代,而收集那些老年代,則是根據(jù)老年代中的垃圾數(shù)量確定的。
單輪的混合收集與年輕代收集無區(qū)別,但是因為老年代垃圾可能比較多,為了滿足暫停的需求,可能會連續(xù)的進(jìn)行多次的混合收集。再過程中,G1會計算下一次處理的CSet集合的分區(qū)數(shù)量,是否本次收集之后需要結(jié)束周期。
Full GC
當(dāng)無法申請新的空間時,會執(zhí)行一次STW式的、單線程的Full GC。Full GC會對整堆做標(biāo)記清除和壓縮,最后將只包含純粹的存活對象。觸發(fā)Full GC的方式:
- 從年輕代分區(qū)拷貝存活對象時,無法找到可用的空閑分區(qū)
- 從老年代分區(qū)轉(zhuǎn)移存活對象時,無法找到可用的空閑分區(qū)
- 分配巨型對象時在老年代無法找到足夠的連續(xù)分區(qū)
流程簡述:
最后
垃圾回收器可以說事Java的基石之一,垃圾回收器的實現(xiàn)充滿了大量的實現(xiàn)細(xì)節(jié),對于一些優(yōu)化十分具有參考價值。