G1收集器:JVM垃圾回收的新一代王者
介紹
G1垃圾收集器在JDK7被開發(fā)出來,JDK8功能基本完全實(shí)現(xiàn)。并且成功替換掉了Parallel Scavenge成為了服務(wù)端模式下默認(rèn)的垃圾收集器。JDK 9以后默認(rèn)使用,替代了CMS 收集器。
G1和CMS一樣,也是采用三色標(biāo)記分段式進(jìn)行回收的算法, 不過它是寫屏障 + STAB快照實(shí)現(xiàn)。
G1 收集器的最大特點(diǎn)
- G1 最大的特點(diǎn)是引入分區(qū)的思路,弱化了分代的概念。
- 并行與并發(fā):G1 能充分利用 CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè) CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時(shí)間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動(dòng)作,G1 收集器仍然可以通過并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行。
- 空間整合:與 CMS 的“標(biāo)記-清除”算法不同,G1 從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,不會(huì)產(chǎn)生空間碎片;從局部上來看是基于“標(biāo)記-復(fù)制”算法實(shí)現(xiàn)的。
- 可預(yù)測(cè)的停頓:G1垃圾回收器設(shè)定了用戶可控的停頓時(shí)間目標(biāo),開發(fā)者可以通過設(shè)置參數(shù)來指定允許的最大垃圾回收停頓時(shí)間。G1會(huì)根據(jù)這個(gè)目標(biāo)來動(dòng)態(tài)調(diào)整回收策略,盡可能地減少長(zhǎng)時(shí)間的垃圾回收停頓。
如何完成可預(yù)測(cè)的? G1根據(jù)歷史數(shù)據(jù)來預(yù)測(cè)本次回收需要的堆分區(qū)數(shù)量,也就是選擇回收哪些內(nèi)存空間。最簡(jiǎn)單的方法就是使用算術(shù)的平均值建立一個(gè)線性關(guān)系來進(jìn)行預(yù)測(cè)。比如:過去10次一共收集了10GB的內(nèi)存,花費(fèi)了1s。那么在200ms的時(shí)間下,最多可以收集2GB的內(nèi)存空間。而G1的預(yù)測(cè)邏輯是基于衰減平均值和衰減標(biāo)準(zhǔn)差來確定的。
CMS 和 G1 的區(qū)別
- CMS 中,堆被分為 PermGen,YoungGen,OldGen ;而 YoungGen 又分了兩個(gè) survivo 區(qū)域。在 G1 中,堆被平均分成幾個(gè)區(qū)域 (region) ,在每個(gè)區(qū)域中,雖然也保留了新老代的概念,但是收集器是以整個(gè)區(qū)域?yàn)閱挝皇占摹?/span>
- G1 在回收內(nèi)存后,會(huì)立即同時(shí)做合并空閑內(nèi)存的工作;而 CMS ,則默認(rèn)是在 STW(stop the world)的時(shí)候做。
- G1 會(huì)在 Young GC 中使用;而 CMS 只能在 Old 區(qū)使用
分區(qū)Region
G1同時(shí)回收新生代和老年代,但是分別被稱為G1的Young GC模式和Mixed GC模式。這個(gè)特性來源于G1獨(dú)特的內(nèi)存布局,內(nèi)存分配不再嚴(yán)格遵守新生代,老年代的劃分,而是以Region為單位,G1跟蹤各個(gè)Region的并且維護(hù)一個(gè)關(guān)于Region的優(yōu)先級(jí)列表。在合適的時(shí)機(jī)選擇合適的Region進(jìn)行回收。這種基于Region的內(nèi)存劃分為一些巧妙的設(shè)計(jì)思想提供了解決停頓時(shí)間和高吞吐的基礎(chǔ)。接下來我們將詳細(xì)講解G1的詳細(xì)垃圾回收過程和里面可圈可點(diǎn)的設(shè)計(jì)。
圖片
G1采用了分區(qū)(Region)的思路,將整個(gè)堆空間分成若干個(gè)大小相等的內(nèi)存區(qū)域,每次分配對(duì)象空間將逐段地使用內(nèi)存。因此,在堆的使用上,G1并不要求對(duì)象的存儲(chǔ)一定是物理上連續(xù)的,只要邏輯上連續(xù)即可;每個(gè)分區(qū)也不會(huì)確定地為某個(gè)代服務(wù),可以按需在年輕代和老年代之間切換。啟動(dòng)時(shí)可以通過參數(shù)-XX:G1HeapReginotallow=n可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認(rèn)將整堆劃分為2048個(gè)分區(qū)
。在分代垃圾回收算法的思想下,region邏輯上劃分為Eden,Survivor和老年代。每個(gè)分區(qū)都可能是eden區(qū),Survivor區(qū)也可能是old區(qū),但在一個(gè)時(shí)刻只能是一種分區(qū)。各種角色的region個(gè)數(shù)都是不固定的,這說明每個(gè)代的內(nèi)存也是不固定的。這些region在邏輯上是連續(xù)的,而不是物理上連續(xù),這點(diǎn)和之前的young/old區(qū)物理連續(xù)很不一樣。
G1對(duì)內(nèi)存的使用以分區(qū)(Region)為單位
- 堆內(nèi)存會(huì)被切分成為很多個(gè)固定大小區(qū)域(Region),每個(gè)是連續(xù)范圍的虛擬內(nèi)存。
- 堆內(nèi)存中一個(gè)區(qū)域 (Region) 的大小,可以通過 -XX:G1HeapRegionSize 參數(shù)指定,大小區(qū)間最小 1M 、最大 32M ,總之是 2 的冪次方。
- 默認(rèn)是將堆內(nèi)存按照 2048 份均分。
圖片
- 每個(gè) Region 被標(biāo)記了 E、S、O 和 H,這些區(qū)域在邏輯上被映射為 Eden,Survivor 和老年代。存活的對(duì)象從一個(gè)區(qū)域轉(zhuǎn)移(即復(fù)制或移動(dòng))到另一個(gè)區(qū)域。區(qū)域被設(shè)計(jì)為并行收集垃圾,可能會(huì)暫停所有應(yīng)用線程。如上圖所示,區(qū)域可以分配到 Eden,survivor 和老年代。
- 巨型區(qū)域(Humongous Region):如果一個(gè)對(duì)象占用的空間超過了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個(gè)巨型對(duì)象。如果對(duì)一個(gè)短期存在的大對(duì)象使用復(fù)制算法回收的話,復(fù)制成本非常高,而直接放進(jìn)old區(qū)則導(dǎo)致原本應(yīng)該短期存在的對(duì)象占用了老年代的內(nèi)存,更加不利于回收性能。為了解決這個(gè)問題,G1劃分了一個(gè)Humongous區(qū),它用來專門存放巨型對(duì)象。如果一個(gè)H區(qū)裝不下一個(gè)巨型對(duì)象,那么G1會(huì)尋找連續(xù)的H分區(qū)來存儲(chǔ)。為了能找到連續(xù)的H區(qū),有時(shí)候不得不啟動(dòng)Full GC。
內(nèi)部數(shù)據(jù)結(jié)構(gòu)
Card Table卡表
Card Table是Region的內(nèi)部結(jié)構(gòu)劃分。每個(gè)region內(nèi)部被劃分為若干的內(nèi)存塊,被稱為card。這些card集合被稱為card table,卡表。
比如下面的例子,region1中的內(nèi)存區(qū)域被劃分為9塊card,9塊card的集合就是卡表card table。
圖片
card表可以記錄每一塊card內(nèi)存區(qū)域是否dirty。如果在發(fā)生YGC時(shí),怎么知道那些是存活對(duì)象,并且其它代區(qū)域有沒有引用這部分對(duì)象,于是把region劃分了很多card區(qū)域, 每個(gè)區(qū)域大小不超過512b,當(dāng)該card區(qū)域里的對(duì)象有引用關(guān)系,將當(dāng)前card置為“dirty”, 并且使用卡表(CardTable)來記錄每一塊card是否dirty,在進(jìn)行GC時(shí),不用遍歷所有的空間, 只需要遍歷卡表中為"dirty"。
圖片
Rset記憶集合
除了卡表,每個(gè)region中都含有Remember Set,簡(jiǎn)稱RSet。RSet其實(shí)是hash表,key為引用本region的其他region起始地址,value為本region中被key對(duì)應(yīng)的region引用的card索引位置。
這里必須講解一下RSet存在的原因,RSet是為了解決"跨代引用"。想象一下,一個(gè)新生代對(duì)象被老年代對(duì)象引用,那么為了通過引用鏈找到這個(gè)新生代對(duì)象,從GC Roots出發(fā)遍歷對(duì)象時(shí)必須經(jīng)過老年代對(duì)象。實(shí)際上以這種方式遍歷時(shí),是把所有對(duì)象都遍歷了一遍。但是我們的其實(shí)只想回收新生代的對(duì)象,卻把所有對(duì)象都遍歷了一遍,這無疑很低效。
在YoungGC時(shí),當(dāng)RSet存在時(shí),順著引用鏈查找引用。如果引用鏈上出現(xiàn)了老年代對(duì)象,那么直接放棄查找這條引用鏈。當(dāng)整個(gè)GC Root Tracing執(zhí)行完畢后,就知道了除被跨代引用外還存活的新生代對(duì)象。緊接著再遍歷新生代Region的RSet,如果RSet里存在key為老年代的Region,就將key對(duì)應(yīng)的value代表的card的對(duì)象標(biāo)記為存活,這樣就標(biāo)記到了被跨代引用的新生代對(duì)象。它可以使得垃圾收集器不需要掃描整個(gè)堆去找到誰的引用了當(dāng)前分區(qū)對(duì)象,是G1高效回收的關(guān)鍵點(diǎn)。
當(dāng)然這么做會(huì)存在一個(gè)問題,如果部分老年代對(duì)象是應(yīng)該被回收的對(duì)象,但還是跨代引用了新生代,會(huì)導(dǎo)致原本應(yīng)該被回收的新生代對(duì)象躲過本輪新生代回收。這部分對(duì)象就只能等到后續(xù)的老年代的垃圾回收mixed GC來回收掉。這也是為什么G1的回收精度比較低的原因之一。
圖片
以這幅圖為例,region1和region2都引用了region3中的對(duì)象,那么region3的RSet中有兩個(gè)key,分別是region1的起始地址和region2的起始地址。在掃描region3的RSet時(shí),發(fā)現(xiàn)key為0x6a的region是一個(gè)old區(qū)region。如果這時(shí)第3,5card對(duì)應(yīng)的對(duì)象沒有被標(biāo)記為可達(dá),那么這里就會(huì)根據(jù)RSet再次標(biāo)記。同樣的,key為0x9b對(duì)應(yīng)的region是一個(gè)young區(qū)域的region,那么0,2號(hào)card的對(duì)象則不會(huì)被標(biāo)記。
事實(shí)上,并非所有的引用都需要記錄在RSet中,如果一個(gè)分區(qū)確定需要掃描,那么無需RSet也可以無遺漏的得到引用關(guān)系。那么引用源自本分區(qū)的對(duì)象,當(dāng)然不用落入RSet中;同時(shí),G1 GC每次都會(huì)對(duì)年輕代進(jìn)行整體收集,因此引用源自年輕代的對(duì)象,也不需要在RSet中記錄。最后只有老年代的分區(qū)可能會(huì)有RSet記錄,這些分區(qū)稱為擁有RSet分區(qū)(an RSet’s owning region)。
Per Region Table (PRT)
RSet在內(nèi)部使用Per Region Table(PRT)記錄分區(qū)的引用情況。由于RSet的記錄要占用分區(qū)的空間,如果一個(gè)分區(qū)非常"受歡迎",那么RSet占用的空間會(huì)上升,從而降低分區(qū)的可用空間。G1應(yīng)對(duì)這個(gè)問題采用了改變RSet的密度的方式,在PRT中將會(huì)以三種模式記錄引用:
- 稀少:直接記錄引用對(duì)象的卡片索引
- 細(xì)粒度:記錄引用對(duì)象的分區(qū)索引
- 粗粒度:只記錄引用情況,每個(gè)分區(qū)對(duì)應(yīng)一個(gè)比特位
由上可知,粗粒度的PRT只是記錄了引用數(shù)量,需要通過整堆掃描才能找出所有引用,因此掃描速度也是最慢的。
RSet和卡表的區(qū)別是什么?
卡表記錄的是堆內(nèi)存中card有沒有變成"dirty", 但是它本身不知道dirty里面哪些是引用了的對(duì)象,它是一個(gè)大維度的一個(gè)記錄,RSet是記錄自身Region中對(duì)象引用了其它Region中的那些對(duì)象,詳細(xì)的記錄對(duì)方引用對(duì)象信息,G1使用了兩者的結(jié)合,實(shí)現(xiàn)了增量式的垃圾回收,并優(yōu)化跨區(qū)引用的最終處理。詳情可以繼續(xù)看后文
堆Heap
G1同樣可以通過-Xms/-Xmx來指定堆空間大小。當(dāng)發(fā)生年輕代收集或混合收集時(shí),通過計(jì)算GC與應(yīng)用的耗費(fèi)時(shí)間比,自動(dòng)調(diào)整堆空間大小。如果GC頻率太高,則通過增加堆尺寸,來減少GC頻率,相應(yīng)地GC占用的時(shí)間也隨之降低;目標(biāo)參數(shù)-XX:GCTimeRatio即為GC與應(yīng)用的耗費(fèi)時(shí)間比,G1默認(rèn)為9,而CMS默認(rèn)為99,因?yàn)镃MS的設(shè)計(jì)原則是耗費(fèi)在GC上的時(shí)間盡可能的少。另外,當(dāng)空間不足,如對(duì)象空間分配或轉(zhuǎn)移失敗時(shí),G1會(huì)首先嘗試增加堆空間,如果擴(kuò)容失敗,則發(fā)起擔(dān)保的Full GC。Full GC后,堆尺寸計(jì)算結(jié)果也會(huì)調(diào)整堆空間。
CSet
Collection SET用于記錄可被回收分區(qū)的集合組, G1使用不同算法,動(dòng)態(tài)的計(jì)算出那些分區(qū)是需要被回收的,將其放到CSet中,在CSet當(dāng)中存活的數(shù)據(jù)都會(huì)在GC過程中拷貝到另一個(gè)可用分區(qū),CSet可以是所有類型分區(qū),它需要額外占用內(nèi)存,堆空間的1%。
CSet收集示意圖
圖片
收集集合(CSet)代表每次GC暫停時(shí)回收的一系列目標(biāo)分區(qū)。在任意一次收集暫停中,CSet所有分區(qū)都會(huì)被釋放,內(nèi)部存活的對(duì)象都會(huì)被轉(zhuǎn)移到分配的空閑分區(qū)中。因此無論是年輕代收集,還是混合收集,工作的機(jī)制都是一致的。年輕代收集CSet只容納年輕代分區(qū),而混合收集會(huì)通過啟發(fā)式算法,在老年代候選回收分區(qū)中,篩選出回收收益最高的分區(qū)添加到CSet中。
候選老年代分區(qū)的CSet準(zhǔn)入條件,可以通過活躍度閾值-XX:G1MixedGCLiveThresholdPercent(默認(rèn)85%)進(jìn)行設(shè)置,從而攔截那些回收開銷巨大的對(duì)象;同時(shí),每次混合收集可以包含候選老年代分區(qū),可根據(jù)CSet對(duì)堆的總大小占比-XX:G1OldCSetRegionThresholdPercent(默認(rèn)10%)設(shè)置數(shù)量上限。
由上述可知,G1的收集都是根據(jù)CSet進(jìn)行操作的,年輕代收集與混合收集沒有明顯的不同,最大的區(qū)別在于兩種收集的觸發(fā)條件。
年輕代收集集合 CSet of Young Collection
應(yīng)用線程不斷活動(dòng)后,年輕代空間會(huì)被逐漸填滿。當(dāng)JVM分配對(duì)象到Eden區(qū)域失敗(Eden區(qū)已滿)時(shí),便會(huì)觸發(fā)一次STW式的年輕代收集。在年輕代收集中,Eden分區(qū)存活的對(duì)象將被拷貝到Survivor分區(qū);原有Survivor分區(qū)存活的對(duì)象,將根據(jù)任期閾值(tenuring threshold)分別晉升到PLAB中,新的survivor分區(qū)和老年代分區(qū)。而原有的年輕代分區(qū)將被整體回收掉。
同時(shí),年輕代收集還負(fù)責(zé)維護(hù)對(duì)象的年齡(存活次數(shù)),輔助判斷老化(tenuring)對(duì)象晉升的時(shí)候是到Survivor分區(qū)還是到老年代分區(qū)。年輕代收集首先先將晉升對(duì)象尺寸總和、對(duì)象年齡信息維護(hù)到年齡表中,再根據(jù)年齡表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默認(rèn)50%)、最大任期閾值-XX:MaxTenuringThreshold(默認(rèn)15),計(jì)算出一個(gè)恰當(dāng)?shù)娜纹陂撝担彩浅^任期閾值的對(duì)象都會(huì)被晉升到老年代。
混合收集集合 CSet of Mixed Collection
年輕代收集不斷活動(dòng)后,老年代的空間也會(huì)被逐漸填充。當(dāng)老年代占用空間超過整堆比IHOP閾值-XX:InitiatingHeapOccupancyPercent(默認(rèn)45%)時(shí),G1就會(huì)啟動(dòng)一次混合垃圾收集周期。為了滿足暫停目標(biāo),G1可能不能一口氣將所有的候選分區(qū)收集掉,因此G1可能會(huì)產(chǎn)生連續(xù)多次的混合收集與應(yīng)用線程交替執(zhí)行,每次STW的混合收集與年輕代收集過程相類似。
為了確定包含到年輕代收集集合CSet的老年代分區(qū),JVM通過參數(shù)混合周期的最大總次數(shù)-XX:G1MixedGCCountTarget(默認(rèn)8)、堆廢物百分比-XX:G1HeapWastePercent(默認(rèn)5%)。通過候選老年代分區(qū)總數(shù)與混合周期最大總次數(shù),確定每次包含到CSet的最小分區(qū)數(shù)量;根據(jù)堆廢物百分比,當(dāng)收集達(dá)到參數(shù)時(shí),不再啟動(dòng)新的混合收集。而每次添加到CSet的分區(qū),則通過計(jì)算得到的GC效率進(jìn)行安排。
Young GC流程
在了解了region的內(nèi)部結(jié)構(gòu)之后,我們?cè)賮砜匆幌翯1的young gc的具體流程。
- stop the world,整個(gè)young gc的流程都是在stw里進(jìn)行的,這也是為什么young gc能回收全部eden區(qū)域的原因??刂苰oung gc開銷的辦法只有減少young region的個(gè)數(shù),也就是減少年輕代內(nèi)存的大小,還有就是并發(fā),多個(gè)線程同時(shí)進(jìn)行g(shù)c,盡量減少stw時(shí)間。
- 掃描GCRoots,注意這里掃描的GC Roots就是一般意義上的GC Roots,是掃描的直接指向young代的對(duì)象,那如果GC Root是直接指向老年代對(duì)象的,則會(huì)直接停止在這一步,也就是不往下掃描了。被老年代對(duì)象指向的young代對(duì)象會(huì)在接下來的利用Rset中key指向老年代的卡表識(shí)別出來,這樣就避免了對(duì)老年代整個(gè)大的heap掃描,提高了效率。這也是為什么Rset能避免對(duì)老年代整體掃描的原因。
- 排空dirty card quene,更新Rset。Rset中記錄了哪些對(duì)象被老年代跨帶引用,也就是當(dāng)新生代對(duì)象被老年代對(duì)象引用時(shí),應(yīng)該更新這個(gè)記錄到RSet中。但更新RSet記錄的時(shí)機(jī)不是伴隨著引用更改馬上發(fā)生的。每當(dāng)老年代引用新生代對(duì)象時(shí),這個(gè)引用記錄對(duì)應(yīng)的card地址其實(shí)會(huì)被放入Dirty Card Queue(線程私有的,當(dāng)線程私有的dirty card queue滿了之后會(huì)被轉(zhuǎn)移到全局的dirty card queue,這個(gè)全局是唯一的),原因是如果每次更新引用時(shí)直接更新Rset會(huì)導(dǎo)致多線程競(jìng)爭(zhēng),因?yàn)橘x值操作很頻繁,影響性能。所以更新Rset交由Refinement線程來進(jìn)行。全局DirtyCardQueue的容量變化分為4個(gè)階段
圖片
白色:無事發(fā)生
綠色:Refinement線程被激活,-XX:G1Cnotallow=N指定的線程個(gè)數(shù)。從(全局和線程私有)隊(duì)列中拿出dirty card。并更新到對(duì)應(yīng)的Rset中。
黃色:產(chǎn)生dirty card的速度過快,激活全部的Refinement線程,通過參數(shù)-XX:G1ConcRefinementYellowZnotallow=N 指定
紅色:產(chǎn)生dirty card的速度過快,將應(yīng)用線程也加入到排空隊(duì)列的工作中。目的是把應(yīng)用線程拖慢,減慢dirty card產(chǎn)生。
- 掃描Rset,掃描所有Rset中Old區(qū)到y(tǒng)oung區(qū)的引用。到這一步就確定出了young區(qū)域哪些對(duì)象是存活的。
- 拷貝對(duì)象到survivor區(qū)域或者晉升old區(qū)域。
- 處理引用隊(duì)列,軟引用,弱引用,虛引用
以上就是young gc的全部流程。
三色標(biāo)記算法的漏標(biāo)問題
知道了Young GC的流程后,接下來我們將學(xué)習(xí)G1針對(duì)老年代的垃圾回收過程Mixed GC,但是在正式開始介紹之前我們先講解一下可達(dá)性分析算法的具體實(shí)現(xiàn),三色標(biāo)記算法。以及三色標(biāo)記算法的缺陷以及G1是如何解決這個(gè)缺陷的。
在可達(dá)性分析的思想指導(dǎo)下,我們需要標(biāo)記對(duì)象是否可達(dá),那么我們采用將對(duì)象標(biāo)記為不同的顏色來區(qū)分對(duì)象是否可達(dá)。可以理解如果一個(gè)對(duì)象能從GC Roots出發(fā)并且遍歷到,那么對(duì)象就是可達(dá)的,這個(gè)過程我們稱為檢查。
- 白色:對(duì)象還沒被檢查。
- 灰色:對(duì)象被檢查了,但是對(duì)象的成員Field(對(duì)象中引用的其他對(duì)象)還沒有被檢查。這說明這個(gè)對(duì)象是可達(dá)的。
- 黑色:對(duì)象被檢查了,對(duì)象的成員Fileld也被檢查了。
那么整個(gè)檢測(cè)的過程,就是從GC Roots出發(fā)不斷地遍歷對(duì)象,并且將可達(dá)的對(duì)象標(biāo)記成黑色的過程。當(dāng)標(biāo)記結(jié)束時(shí),還是白色的對(duì)象就是沒被遍歷到的對(duì)象,即不可達(dá)的對(duì)象。
舉個(gè)例子
第一輪檢查,找到所有的GC Roots,GC Roots被標(biāo)記為灰色,有的GC Roots因?yàn)闆]有成員Field則被標(biāo)記為黑色。
圖片
第二輪檢查,檢查被GC Roots引用的對(duì)象,并標(biāo)記為灰色
圖片
第三輪檢查,循環(huán)之前的步驟,將被標(biāo)記為灰色對(duì)象的子Field檢查。因?yàn)檫@里就假設(shè)了3次循環(huán)檢查的對(duì)象,所以是最后一次檢查。這一路檢查結(jié)束,還是白色的對(duì)象就是可以被回收的對(duì)象。即圖例里的objectC
圖片
以上描述的是一輪三色標(biāo)記算法的工作過程,但是這是一個(gè)理想情況。但是在標(biāo)記過程中,標(biāo)記的線程是和用戶線程交替運(yùn)行的,所以可能出現(xiàn)標(biāo)記過程中引用發(fā)生變化的情況。
- 已經(jīng)存在的對(duì)象被漏標(biāo):在第二輪檢查到第三輪檢查之間,假設(shè)發(fā)生了引用的變化,objectD不再被objectB引用,而是被objectA引用,而且此時(shí)ObjectA的成員已經(jīng)被檢查完畢了,objectB的成員Field還沒被檢查。這時(shí),objectD就永遠(yuǎn)不會(huì)再被檢查到。這就導(dǎo)致了漏標(biāo)。
- 新產(chǎn)生的對(duì)象被漏標(biāo):這個(gè)對(duì)象被已經(jīng)被標(biāo)記為黑色的對(duì)象持有。比如圖例中的newObjectF。因?yàn)楹谏珜?duì)象已經(jīng)被認(rèn)為是檢查完畢了,所以新產(chǎn)生的對(duì)象不會(huì)再被檢查,這也會(huì)導(dǎo)致漏標(biāo)。
有兩種被漏標(biāo)的情況
已經(jīng)存在的對(duì)象被漏標(biāo)
即圖例中被漏標(biāo)的objectD,要漏標(biāo)objectD,必須同時(shí)滿足:
- 灰色對(duì)象不再指向白色對(duì)象,即objectB.d = null
- 黑色對(duì)象指向白色對(duì)象,即objectA.d = objectD
要解決漏標(biāo),只要打破這兩個(gè)條件的任意一個(gè)即可。由此我們引出兩個(gè)解決方案。原始快照和增量更新。
- 原始快照(Snapshot At The Beginning,簡(jiǎn)稱SATB): 當(dāng)任意的灰色對(duì)象到白色對(duì)象的引用被刪除時(shí),記錄下這個(gè)被刪除的引用,默認(rèn)這個(gè)被刪除的引用對(duì)象是存活的。這也可以理解為整個(gè)檢查過程中的引用關(guān)系以檢查剛開始的那一刻為準(zhǔn)。
- 增量更新(Incremental Update): 當(dāng)黑色對(duì)象被新增一個(gè)白色對(duì)象的引用的時(shí)候,記錄下發(fā)生引用變更的黑色對(duì)象,并將它重新改變?yōu)榛疑珜?duì)象,重新標(biāo)記。這是CMS采用的解決辦法
在上面的兩種解決方案里,我們發(fā)現(xiàn),無論如何,都要記錄下發(fā)生更改的引用。所以需要一種記錄引用發(fā)生更改的手段,寫屏障(write barrier)。寫屏障是一種記錄下引用發(fā)生變更的手段,效果類似AOP,但是其實(shí)現(xiàn)遠(yuǎn)比我們使用的AOP更加底層,可以認(rèn)為是在JVM代碼層面的一段代碼。每當(dāng)任意的引用變更時(shí),就會(huì)觸發(fā)這段代碼,并記錄下發(fā)生變更的引用。
新產(chǎn)生的對(duì)象被漏標(biāo)
新產(chǎn)生的對(duì)象被漏標(biāo)的解決方式則簡(jiǎn)單一些,在增量更新模式下,這個(gè)問題天生就被解決了。在SATB模式下,其實(shí)是在檢查一開始就確定了一個(gè)檢查范圍,所以可以將新產(chǎn)生的對(duì)象放在檢查范圍之外,默認(rèn)新產(chǎn)生的對(duì)象是存活的。當(dāng)然這個(gè)過程得實(shí)際結(jié)合卡表來講解才會(huì)更加具體形象。接下來在Mixed GC的過程里再細(xì)說。
SATB
Snapshot At The Beginning,G1在分配對(duì)象時(shí),會(huì)在region中有2個(gè)top-at-mark-start(TAMS)指針,分別表示prevTAMS和nextTAMS。對(duì)應(yīng)著卡表上即指向表示卡表范圍的的兩個(gè)編號(hào),GC是分配在nextTAMS位置以上的對(duì)象都視為活著的,這是一種隱式的標(biāo)記(這涉及到G1 MixedGC垃圾回收階段的細(xì)節(jié),很復(fù)雜,接下來會(huì)詳細(xì)討論)。這種解決漏標(biāo)的方式是有缺陷的,它會(huì)造成真正應(yīng)該被回收的白對(duì)象躲過這次GC生存到下一次GC,這就是float garbage(浮動(dòng)垃圾)。因?yàn)镾ATB的做法精度比較低,所以造成float garbage的情況也會(huì)比較多。
圖片
為什么G1采用SATB而不用incremental update?
SATB算法:是一種基于快照的算法,它可以避免在垃圾回收時(shí)出現(xiàn)對(duì)象漏標(biāo)或者重復(fù)標(biāo)記的問題,從而提高垃圾回收的準(zhǔn)確性和效率,在垃圾回收開始時(shí),對(duì)堆中的對(duì)象引用進(jìn)行快照,然后在并發(fā)標(biāo)記階段中記錄下所有被修改過對(duì)象引用,保存到satb_mark_queue中,最后在重新標(biāo)記階段重新掃描這些對(duì)象,標(biāo)記所有被修改的對(duì)象,保證了準(zhǔn)確性和效率。
因?yàn)椴捎胕ncremental update把黑色重新標(biāo)記為灰色后,之前掃描過的還要再掃描一遍,效率太低。G1有RSet與SATB相配合。Card Table里記錄了RSet,RSet里記錄了其他對(duì)象指向自己的引用,這樣就不需要再掃描其他區(qū)域,只要掃描RSet就可以了。
也就是說 灰色–>白色 引用消失時(shí),如果沒有 黑色–>白色,引用會(huì)被push到堆棧,下次掃描時(shí)拿到這個(gè)引用,由于有RSet的存在,不需要掃描整個(gè)堆去查找指向白色的引用,效率比較高。SATB配合RSet渾然天成
Mixed GC 流程
Mixed GC從步驟上可以分為兩個(gè)大步驟,全局并發(fā)標(biāo)記(global concurrent marking),拷貝存活對(duì)象(evacuation)。全局并發(fā)表的過程涉及到SATB的標(biāo)記過程,我們將詳細(xì)講解。
全局并發(fā)標(biāo)記(global concurrent marking)
G1收集器垃圾收集器的全局并發(fā)標(biāo)記(global concurrent marking)分為多個(gè)階段
- 初始標(biāo)記(initial marking): 這個(gè)階段會(huì)STW,標(biāo)記從GC Root開始直接可達(dá)的對(duì)象,這一步伴隨著young gc。之所以要young gc是為了處理跨代引用,老年代獨(dú)享也可能被年輕代跨代引用,但是老年代不能使用RSet來解決跨代引用。還有就是young gc也會(huì)stw,在第一步y(tǒng)oung gc可以共用stw的時(shí)間,盡量減少stw時(shí)間。這一步還初始化了一些參數(shù),將bottom指針賦值給prevTAMS指針,top指針賦值給nextTAMS指針,同時(shí)清空nextBitMap指針。因?yàn)橹蟮牟l(fā)標(biāo)記需要使用到這三個(gè)變量。
top,prevTAMS,nextTAMS,top都是指向卡表的指針,他們的存在是為了標(biāo)識(shí)哪些對(duì)象是可以被回收的,哪些是存活的,這就是SATB機(jī)制。而nextBitMap則是記錄下卡表中哪些對(duì)象是存活的一個(gè)數(shù)組,當(dāng)然現(xiàn)在還沒開始檢查,nextBitMap里的記錄都是空。
圖片
- 根分區(qū)掃描(root region scan): 這個(gè)階段在stw之后,會(huì)掃描survivor區(qū)域(survivor分區(qū)就是根分區(qū)),將所有被survivor區(qū)域?qū)ο笠玫睦夏甏鷮?duì)象標(biāo)記。這也是上一步需要young gc的原因,處理跨代引用時(shí)需要知道哪些old區(qū)對(duì)象被S區(qū)對(duì)象引用。這個(gè)過程因?yàn)樾枰獟呙鑣urvivor分區(qū),所以不能發(fā)生young gc,如果掃描過程中新生代被耗盡,那么必須等待掃描結(jié)束才可以開始young gc。這一步耗時(shí)很短。
- 并發(fā)標(biāo)記(Concurrent Marking) 從GC Roots開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出各個(gè)region的存活對(duì)象信息,耗時(shí)較長(zhǎng)。粗略過程是這樣的,但實(shí)際這一步的過程很復(fù)雜。因?yàn)橐紤]在SATB機(jī)制之下,各個(gè)指針的變化。 假設(shè)在根分區(qū)掃描后沒有引用的改變,那么一個(gè)region的分區(qū)狀態(tài)和第一步init marking初始化完一致。此時(shí)如果再繼續(xù)分配對(duì)象,那么對(duì)象會(huì)分配在nextTAMS之后,隨著對(duì)象的分配,TOP指針會(huì)向后移動(dòng)。
圖片
因?yàn)檫@一步是和mutator(用戶線程)并發(fā)運(yùn)行的,所以從根節(jié)點(diǎn)掃描的時(shí)候其實(shí)是掃描的一個(gè)快照snapshot,快照位置就是prevTAMS到nextTAMS(注意快照位置是不變的,但是prevTAMS到nextTAMS之間的對(duì)象在掃描過程中會(huì)改變)。 當(dāng)region中分配新對(duì)象時(shí),新對(duì)象都會(huì)分配在nextTAMS之后,這導(dǎo)致top指向的位置也往后移動(dòng),nextTAMS和top之間選哪個(gè)都是被認(rèn)為隱式存活。 還有這期間也有可能應(yīng)該被掃描的位置prevTAMS和nextTAMS之間的位置引用發(fā)生了變化,比如白色對(duì)象被黑色對(duì)象持有了,這就是三色標(biāo)記算法的缺陷,需要更改白色對(duì)象的狀態(tài)。這里會(huì)將引用被更改的對(duì)象放入satb_mark_queue。satb_mark_queue是一個(gè)隊(duì)列,里面記錄所有被改變引用關(guān)系的白色對(duì)象。這里指的satb_mark_queue指的全局的queue。除了全局的queue,每個(gè)線程也有自己的satb mark queue,全局的queue的引用是由所有其他線程的satb mark queue合并得來的,線程的satb mark queu滿了會(huì)被轉(zhuǎn)移到全局satb mark queue。且并發(fā)標(biāo)記階段會(huì)定期檢查全局satb mark queue的容量,超過某個(gè)容量就concurrent marker線程就會(huì)將全局satb mark que和線程satb mark que的對(duì)象都取出來全部標(biāo)記上,當(dāng)然也會(huì)將這些對(duì)象的子field全部壓棧(marking stack)等待接下來被標(biāo)記到,這個(gè)處理類似于全局dirty card quene。這里注意。
圖片
隨著并發(fā)標(biāo)記結(jié)束nextBitMap里也標(biāo)記了哪些對(duì)象是可以回收的,但注意,不一定每個(gè)線程里satb mark queue都被轉(zhuǎn)移到了全局的satb mark queue,因?yàn)楹喜⑦@個(gè)過程也是并發(fā)的。所以需要下一步
- 最終標(biāo)記(remark): 標(biāo)記那些并發(fā)標(biāo)記階段發(fā)生變化的對(duì)象,就是將線程satb mark queue中引用發(fā)生更改的對(duì)象找出來,放入satb mark queue。這個(gè)階段為了保證標(biāo)記正確必須STW。
- 清點(diǎn)垃圾(cleanup): 對(duì)各個(gè)region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶期待的GC停頓時(shí)間指定回收計(jì)劃,選中部分old region,和全部的young region,這些被選中的分區(qū)稱為Collection Set(Cset),還會(huì)把沒有任何對(duì)象的region加入到可用來分配對(duì)象的region集合中。注意這一步不是清除,是清點(diǎn)出哪些region值得回收,不會(huì)復(fù)制任何對(duì)象。清點(diǎn)執(zhí)行完,一個(gè)全局并發(fā)標(biāo)記周期基本就執(zhí)行完了。這時(shí)還會(huì)將nextTAMS指針賦值給prevTAMS,且nextBitMap賦值給prevBitMap。
這里是不是很奇怪為什么要記錄本輪標(biāo)記的結(jié)果到prevBitMap,難道下次再來檢查本region時(shí)還可以再復(fù)用這個(gè)標(biāo)記結(jié)果嗎。 我們知道G1是可以根據(jù)內(nèi)存的變化自己調(diào)整內(nèi)存中E區(qū),O區(qū)的容量的,如果其中某些分區(qū)容量增長(zhǎng)比較快,說明這個(gè)分區(qū)的內(nèi)存訪問更頻繁,在未來也可能更快地達(dá)到region的容量限制,那么下次復(fù)制轉(zhuǎn)移時(shí)就會(huì)優(yōu)先將這塊region中的對(duì)象轉(zhuǎn)移到更大的region中去。
拷貝存活對(duì)象evacuation
標(biāo)記結(jié)束剩下的就是轉(zhuǎn)移evacuation,拷貝存活對(duì)象。就是將活著的對(duì)象拷貝到空的region,再回收掉部分region。這一步是采用多線程復(fù)制清除,整個(gè)過程會(huì)STW。這也是G1的優(yōu)勢(shì)之一,只要還有一塊空閑的region,就可以完成垃圾回收。而不用像CMS那樣必須預(yù)留太多的內(nèi)存。
G1 的活動(dòng)周期
G1垃圾收集活動(dòng)匯總
圖片
RSet的維護(hù)
由于不能整堆掃描,又需要計(jì)算分區(qū)確切的活躍度,因此,G1需要一個(gè)增量式的完全標(biāo)記并發(fā)算法,通過維護(hù)RSet,得到準(zhǔn)確的分區(qū)引用信息。在G1中,RSet的維護(hù)主要來源兩個(gè)方面:寫柵欄(Write Barrier)和并發(fā)優(yōu)化線程(Concurrence Refinement Threads)
柵欄Barrier
柵欄代碼示意
圖片
柵欄是指在原生代碼片段中,當(dāng)某些語句被執(zhí)行時(shí),柵欄代碼也會(huì)被執(zhí)行。而G1主要在賦值語句中,使用寫前柵欄(Pre-Write Barrrier)和寫后柵欄(Post-Write Barrrier)。事實(shí)上,寫柵欄的指令序列開銷非常昂貴,應(yīng)用吞吐量也會(huì)根據(jù)柵欄復(fù)雜度而降低。
寫前柵欄 Pre-Write Barrrier
即將執(zhí)行一段賦值語句時(shí),等式左側(cè)對(duì)象將修改引用到另一個(gè)對(duì)象,那么等式左側(cè)對(duì)象原先引用的對(duì)象所在分區(qū)將因此喪失一個(gè)引用,那么JVM就需要在賦值語句生效之前,記錄喪失引用的對(duì)象。JVM并不會(huì)立即維護(hù)RSet,而是通過批量處理,在將來RSet更新(見SATB)。
寫后柵欄 Post-Write Barrrier
當(dāng)執(zhí)行一段賦值語句后,等式右側(cè)對(duì)象獲取了左側(cè)對(duì)象的引用,那么等式右側(cè)對(duì)象所在分區(qū)的RSet也應(yīng)該得到更新。同樣為了降低開銷,寫后柵欄發(fā)生后,RSet也不會(huì)立即更新,同樣只是記錄此次更新日志,在將來批量處理(見Concurrence Refinement Threads)。
起始快照算法Snapshot at the beginning (SATB)
Taiichi Tuasa貢獻(xiàn)的增量式完全并發(fā)標(biāo)記算法起始快照算法(SATB),主要針對(duì)標(biāo)記-清除垃圾收集器的并發(fā)標(biāo)記階段,非常適合G1的分區(qū)塊的堆結(jié)構(gòu),同時(shí)解決了CMS的主要煩惱:重新標(biāo)記暫停時(shí)間長(zhǎng)帶來的潛在風(fēng)險(xiǎn)。
SATB會(huì)創(chuàng)建一個(gè)對(duì)象圖,相當(dāng)于堆的邏輯快照,從而確保并發(fā)標(biāo)記階段所有的垃圾對(duì)象都能通過快照被鑒別出來。當(dāng)賦值語句發(fā)生時(shí),應(yīng)用將會(huì)改變了它的對(duì)象圖,那么JVM需要記錄被覆蓋的對(duì)象。因此寫前柵欄會(huì)在引用變更前,將值記錄在SATB日志或緩沖區(qū)中。每個(gè)線程都會(huì)獨(dú)占一個(gè)SATB緩沖區(qū),初始有256條記錄空間。當(dāng)空間用盡時(shí),線程會(huì)分配新的SATB緩沖區(qū)繼續(xù)使用,而原有的緩沖去則加入全局列表中。最終在并發(fā)標(biāo)記階段,并發(fā)標(biāo)記線程(Concurrent Marking Threads)在標(biāo)記的同時(shí),還會(huì)定期檢查和處理全局緩沖區(qū)列表的記錄,然后根據(jù)標(biāo)記位圖分片的標(biāo)記位,掃描引用字段來更新RSet。此過程又稱為并發(fā)標(biāo)記/SATB寫前柵欄。
并發(fā)優(yōu)化線程Concurrence Refinement Threads
G1中使用基于Urs H?lzle的快速寫柵欄,將柵欄開銷縮減到2個(gè)額外的指令。柵欄將會(huì)更新一個(gè)card table type的結(jié)構(gòu)來跟蹤代間引用。
當(dāng)賦值語句發(fā)生后,寫后柵欄會(huì)先通過G1的過濾技術(shù)判斷是否是跨分區(qū)的引用更新,并將跨分區(qū)更新對(duì)象的卡片加入緩沖區(qū)序列,即更新日志緩沖區(qū)或臟卡片隊(duì)列。與SATB類似,一旦日志緩沖區(qū)用盡,則分配一個(gè)新的日志緩沖區(qū),并將原來的緩沖區(qū)加入全局列表中。
并發(fā)優(yōu)化線程(Concurrence Refinement Threads),只專注掃描日志緩沖區(qū)記錄的卡片來維護(hù)更新RSet,線程最大數(shù)目可通過-XX:G1ConcRefinementThreads(默認(rèn)等于-XX:ParellelGCThreads)設(shè)置。并發(fā)優(yōu)化線程永遠(yuǎn)是活躍的,一旦發(fā)現(xiàn)全局列表有記錄存在,就開始并發(fā)處理。如果記錄增長(zhǎng)很快或者來不及處理,那么通過閾值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1會(huì)用分層的方式調(diào)度,使更多的線程處理全局列表。如果并發(fā)優(yōu)化線程也不能跟上緩沖區(qū)數(shù)量,則Mutator線程(Java應(yīng)用線程)會(huì)掛起應(yīng)用并被加進(jìn)來幫助處理,直到全部處理完。因此,必須避免此類場(chǎng)景出現(xiàn)。
并發(fā)標(biāo)記周期 Concurrent Marking Cycle
并發(fā)標(biāo)記周期是G1中非常重要的階段,這個(gè)階段將會(huì)為混合收集周期識(shí)別垃圾最多的老年代分區(qū)。整個(gè)周期完成根標(biāo)記、識(shí)別所有(可能)存活對(duì)象,并計(jì)算每個(gè)分區(qū)的活躍度,從而確定GC效率等級(jí)。
當(dāng)達(dá)到IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認(rèn)45%)時(shí),便會(huì)觸發(fā)并發(fā)標(biāo)記周期。整個(gè)并發(fā)標(biāo)記周期將由初始標(biāo)記(Initial Mark)、根分區(qū)掃描(Root Region Scanning)、并發(fā)標(biāo)記(Concurrent Marking)、重新標(biāo)記(Remark)、清除(Cleanup)幾個(gè)階段組成。其中,初始標(biāo)記(隨年輕代收集一起活動(dòng))、重新標(biāo)記、清除是STW的,而并發(fā)標(biāo)記如果來不及標(biāo)記存活對(duì)象,則可能在并發(fā)標(biāo)記過程中,G1又觸發(fā)了幾次年輕代收集。
并發(fā)標(biāo)記線程 Concurrent Marking Threads
并發(fā)標(biāo)記位圖過程
圖片
要標(biāo)記存活的對(duì)象,每個(gè)分區(qū)都需要?jiǎng)?chuàng)建位圖(Bitmap)信息來存儲(chǔ)標(biāo)記數(shù)據(jù),來確定標(biāo)記周期內(nèi)被分配的對(duì)象。G1采用了兩個(gè)位圖Previous Bitmap、Next Bitmap,來存儲(chǔ)標(biāo)記數(shù)據(jù),Previous位圖存儲(chǔ)上次的標(biāo)記數(shù)據(jù),Next位圖在標(biāo)記周期內(nèi)不斷變化更新,同時(shí)Previous位圖的標(biāo)記數(shù)據(jù)也越來越過時(shí),當(dāng)標(biāo)記周期結(jié)束后Next位圖便替換Previous位圖,成為上次標(biāo)記的位圖。同時(shí),每個(gè)分區(qū)通過頂部開始標(biāo)記(TAMS),來記錄已標(biāo)記過的內(nèi)存范圍。同樣的,G1使用了兩個(gè)頂部開始標(biāo)記Previous TAMS(PTAMS)、Next TAMS(NTAMS),記錄已標(biāo)記的范圍。
在并發(fā)標(biāo)記階段,G1會(huì)根據(jù)參數(shù)-XX:ConcGCThreads(默認(rèn)GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4),分配并發(fā)標(biāo)記線程(Concurrent Marking Threads),進(jìn)行標(biāo)記活動(dòng)。每個(gè)并發(fā)線程一次只掃描一個(gè)分區(qū),并通過"手指"指針的方式優(yōu)化獲取分區(qū)。并發(fā)標(biāo)記線程是爆發(fā)式的,在給定的時(shí)間段拼命干活,然后休息一段時(shí)間,再拼命干活。
每個(gè)并發(fā)標(biāo)記周期,在初始標(biāo)記STW的最后,G1會(huì)分配一個(gè)空的Next位圖和一個(gè)指向分區(qū)頂部(Top)的NTAMS標(biāo)記。Previous位圖記錄的上次標(biāo)記數(shù)據(jù),上次的標(biāo)記位置,即PTAMS,在PTAMS與分區(qū)底部(Bottom)的范圍內(nèi),所有的存活對(duì)象都已被標(biāo)記。那么,在PTAMS與Top之間的對(duì)象都將是隱式存活(Implicitly Live)對(duì)象。在并發(fā)標(biāo)記階段,Next位圖吸收了Previous位圖的標(biāo)記數(shù)據(jù),同時(shí)每個(gè)分區(qū)都會(huì)有新的對(duì)象分配,則Top與NTAMS分離,前往更高的地址空間。在并發(fā)標(biāo)記的一次標(biāo)記中,并發(fā)標(biāo)記線程將找出NTAMS與PTAMS之間的所有存活對(duì)象,將標(biāo)記數(shù)據(jù)存儲(chǔ)在Next位圖中。同時(shí),在NTAMS與Top之間的對(duì)象即成為已標(biāo)記對(duì)象。如此不斷地更新Next位圖信息,并在清除階段與Previous位圖交換角色。
初始標(biāo)記 Initial Mark
初始標(biāo)記(Initial Mark)負(fù)責(zé)標(biāo)記所有能被直接可達(dá)的根對(duì)象(原生棧對(duì)象、全局對(duì)象、JNI對(duì)象),根是對(duì)象圖的起點(diǎn),因此初始標(biāo)記需要將Mutator線程(Java應(yīng)用線程)暫停掉,也就是需要一個(gè)STW的時(shí)間段。事實(shí)上,當(dāng)達(dá)到IHOP閾值時(shí),G1并不會(huì)立即發(fā)起并發(fā)標(biāo)記周期,而是等待下一次年輕代收集,利用年輕代收集的STW時(shí)間段,完成初始標(biāo)記,這種方式稱為借道(Piggybacking)。在初始標(biāo)記暫停中,分區(qū)的NTAMS都被設(shè)置到分區(qū)頂部Top,初始標(biāo)記是并發(fā)執(zhí)行,直到所有的分區(qū)處理完。
根分區(qū)掃描 Root Region Scanning
在初始標(biāo)記暫停結(jié)束后,年輕代收集也完成的對(duì)象復(fù)制到Survivor的工作,應(yīng)用線程開始活躍起來。此時(shí)為了保證標(biāo)記算法的正確性,所有新復(fù)制到Survivor分區(qū)的對(duì)象,都需要被掃描并標(biāo)記成根,這個(gè)過程稱為根分區(qū)掃描(Root Region Scanning),同時(shí)掃描的Suvivor分區(qū)也被稱為根分區(qū)(Root Region)。根分區(qū)掃描必須在下一次年輕代垃圾收集啟動(dòng)前完成(并發(fā)標(biāo)記的過程中,可能會(huì)被若干次年輕代垃圾收集打斷),因?yàn)槊看蜧C會(huì)產(chǎn)生新的存活對(duì)象集合。
并發(fā)標(biāo)記 Concurrent Marking
和應(yīng)用線程并發(fā)執(zhí)行,并發(fā)標(biāo)記線程在并發(fā)標(biāo)記階段啟動(dòng),由參數(shù)-XX:ConcGCThreads(默認(rèn)GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4)控制啟動(dòng)數(shù)量,每個(gè)線程每次只掃描一個(gè)分區(qū),從而標(biāo)記出存活對(duì)象圖。在這一階段會(huì)處理Previous/Next標(biāo)記位圖,掃描標(biāo)記對(duì)象的引用字段。同時(shí),并發(fā)標(biāo)記線程還會(huì)定期檢查和處理STAB全局緩沖區(qū)列表的記錄,更新對(duì)象引用信息。參數(shù)-XX:+ClassUnloadingWithConcurrentMark會(huì)開啟一個(gè)優(yōu)化,如果一個(gè)類不可達(dá)(不是對(duì)象不可達(dá)),則在重新標(biāo)記階段,這個(gè)類就會(huì)被直接卸載。所有的標(biāo)記任務(wù)必須在堆滿前就完成掃描,如果并發(fā)標(biāo)記耗時(shí)很長(zhǎng),那么有可能在并發(fā)標(biāo)記過程中,又經(jīng)歷了幾次年輕代收集。如果堆滿前沒有完成標(biāo)記任務(wù),則會(huì)觸發(fā)擔(dān)保機(jī)制,經(jīng)歷一次長(zhǎng)時(shí)間的串行Full GC。
存活數(shù)據(jù)計(jì)算 Live Data Accounting
存活數(shù)據(jù)計(jì)算(Live Data Accounting)是標(biāo)記操作的附加產(chǎn)物,只要一個(gè)對(duì)象被標(biāo)記,同時(shí)會(huì)被計(jì)算字節(jié)數(shù),并計(jì)入分區(qū)空間。只有NTAMS以下的對(duì)象會(huì)被標(biāo)記和計(jì)算,在標(biāo)記周期的最后,Next位圖將被清空,等待下次標(biāo)記周期。
重新標(biāo)記 Remark
重新標(biāo)記(Remark)是最后一個(gè)標(biāo)記階段。在該階段中,G1需要一個(gè)暫停的時(shí)間,去處理剩下的SATB日志緩沖區(qū)和所有更新,找出所有未被訪問的存活對(duì)象,同時(shí)安全完成存活數(shù)據(jù)計(jì)算。這個(gè)階段也是并行執(zhí)行的,通過參數(shù)-XX:ParallelGCThread可設(shè)置GC暫停時(shí)可用的GC線程數(shù)。同時(shí),引用處理也是重新標(biāo)記階段的一部分,所有重度使用引用對(duì)象(弱引用、軟引用、虛引用、最終引用)的應(yīng)用都會(huì)在引用處理上產(chǎn)生開銷。
清除 Cleanup
緊挨著重新標(biāo)記階段的清除(Clean)階段也是STW的。Previous/Next標(biāo)記位圖、以及PTAMS/NTAMS,都會(huì)在清除階段交換角色。清除階段主要執(zhí)行以下操作:
- RSet梳理,啟發(fā)式算法會(huì)根據(jù)活躍度和RSet尺寸對(duì)分區(qū)定義不同等級(jí),同時(shí)RSet數(shù)理也有助于發(fā)現(xiàn)無用的引用。參數(shù)-XX:+PrintAdaptiveSizePolicy可以開啟打印啟發(fā)式算法決策細(xì)節(jié);
- 整理堆分區(qū),為混合收集周期識(shí)別回收收益高(基于釋放空間和暫停目標(biāo))的老年代分區(qū)集合;
- 識(shí)別所有空閑分區(qū),即發(fā)現(xiàn)無存活對(duì)象的分區(qū)。該分區(qū)可在清除階段直接回收,無需等待下次收集周期。
年輕代收集/混合收集周期
年輕代收集和混合收集周期,是G1回收空間的主要活動(dòng)。當(dāng)應(yīng)用運(yùn)行開始時(shí),堆內(nèi)存可用空間還比較大,只會(huì)在年輕代滿時(shí),觸發(fā)年輕代收集;隨著老年代內(nèi)存增長(zhǎng),當(dāng)?shù)竭_(dá)IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認(rèn)45%)時(shí),G1開始著手準(zhǔn)備收集老年代空間。首先經(jīng)歷并發(fā)標(biāo)記周期,識(shí)別出高收益的老年代分區(qū),前文已述。但隨后G1并不會(huì)馬上開始一次混合收集,而是讓應(yīng)用線程先運(yùn)行一段時(shí)間,等待觸發(fā)一次年輕代收集。在這次STW中,G1將保準(zhǔn)整理混合收集周期。接著再次讓應(yīng)用線程運(yùn)行,當(dāng)接下來的幾次年輕代收集時(shí),將會(huì)有老年代分區(qū)加入到CSet中,即觸發(fā)混合收集,這些連續(xù)多次的混合收集稱為混合收集周期(Mixed Collection Cycle)。
GC工作線程數(shù)
GC工作線程數(shù) -XX:ParallelGCThreads
JVM可以通過參數(shù)-XX:ParallelGCThreads進(jìn)行指定GC工作的線程數(shù)量。參數(shù)-XX:ParallelGCThreads默認(rèn)值并不是固定的,而是根據(jù)當(dāng)前的CPU資源進(jìn)行計(jì)算。如果用戶沒有指定,且CPU小于等于8,則默認(rèn)與CPU核數(shù)相等;若CPU大于8,則默認(rèn)JVM會(huì)經(jīng)過計(jì)算得到一個(gè)小于CPU核數(shù)的線程數(shù);當(dāng)然也可以人工指定與CPU核數(shù)相等。
年輕代收集 Young Collection
每次收集過程中,既有并行執(zhí)行的活動(dòng),也有串行執(zhí)行的活動(dòng),但都可以是多線程的。在并行執(zhí)行的任務(wù)中,如果某個(gè)任務(wù)過重,會(huì)導(dǎo)致其他線程在等待某項(xiàng)任務(wù)的處理,需要對(duì)這些地方進(jìn)行優(yōu)化。
并行活動(dòng)
- 外部根分區(qū)掃描 Ext Root Scanning:此活動(dòng)對(duì)堆外的根(JVM系統(tǒng)目錄、VM數(shù)據(jù)結(jié)構(gòu)、JNI線程句柄、硬件寄存器、全局變量、線程對(duì)棧根)進(jìn)行掃描,發(fā)現(xiàn)那些沒有加入到暫停收集集合CSet中的對(duì)象。如果系統(tǒng)目錄(單根)擁有大量加載的類,最終可能其他并行活動(dòng)結(jié)束后,該活動(dòng)依然沒有結(jié)束而帶來的等待時(shí)間。
- 更新已記憶集合 Update RS:并發(fā)優(yōu)化線程會(huì)對(duì)臟卡片的分區(qū)進(jìn)行掃描更新日志緩沖區(qū)來更新RSet,但只會(huì)處理全局緩沖列表。作為補(bǔ)充,所有被記錄但是還沒有被優(yōu)化線程處理的剩余緩沖區(qū),會(huì)在該階段處理,變成已處理緩沖區(qū)(Processed Buffers)。為了限制花在更新RSet的時(shí)間,可以設(shè)置暫停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默認(rèn)10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志緩沖區(qū)更新的任務(wù)不降低,單純地減少RSet的更新時(shí)間,會(huì)導(dǎo)致暫停中被處理的緩沖區(qū)減少,將日志緩沖區(qū)更新工作推到并發(fā)優(yōu)化線程上,從而增加對(duì)Java應(yīng)用線程資源的爭(zhēng)奪。
- RSet掃描 Scan RS:在收集當(dāng)前CSet之前,考慮到分區(qū)外的引用,必須掃描CSet分區(qū)的RSet。如果RSet發(fā)生粗化,則會(huì)增加RSet的掃描時(shí)間。開啟診斷模式-XX:UnlockDiagnosticVMOptions后,通過參數(shù)-XX:+G1SummarizeRSetStats可以確定并發(fā)優(yōu)化線程是否能夠及時(shí)處理更新日志緩沖區(qū),并提供更多的信息,來幫助為RSet粗化總數(shù)提供窗口。參數(shù)-XX:G1SummarizeRSetStatsPeriod=n可設(shè)置RSet的統(tǒng)計(jì)周期,即經(jīng)歷多少此GC后進(jìn)行一次統(tǒng)計(jì)
- 代碼根掃描 Code Root Scanning:對(duì)代碼根集合進(jìn)行掃描,掃描JVM編譯后代碼Native Method的引用信息(nmethod掃描),進(jìn)行RSet掃描。事實(shí)上,只有CSet分區(qū)中的RSet有強(qiáng)代碼根時(shí),才會(huì)做nmethod掃描,查找對(duì)CSet的引用。
- 轉(zhuǎn)移和回收 Object Copy:通過選定的CSet以及CSet分區(qū)完整的引用集,將執(zhí)行暫停時(shí)間的主要部分:CSet分區(qū)存活對(duì)象的轉(zhuǎn)移、CSet分區(qū)空間的回收。通過工作竊取機(jī)制來負(fù)載均衡地選定復(fù)制對(duì)象的線程,并且復(fù)制和掃描對(duì)象被轉(zhuǎn)移的存活對(duì)象將拷貝到每個(gè)GC線程分配緩沖區(qū)GCLAB。G1會(huì)通過計(jì)算,預(yù)測(cè)分區(qū)復(fù)制所花費(fèi)的時(shí)間,從而調(diào)整年輕代的尺寸。
- 終止 Termination:完成上述任務(wù)后,如果任務(wù)隊(duì)列已空,則工作線程會(huì)發(fā)起終止要求。如果還有其他線程繼續(xù)工作,空閑的線程會(huì)通過工作竊取機(jī)制嘗試幫助其他線程處理。而單獨(dú)執(zhí)行根分區(qū)掃描的線程,如果任務(wù)過重,最終會(huì)晚于終止。
- GC外部的并行活動(dòng) GC Worker Other:該部分并非GC的活動(dòng),而是JVM的活動(dòng)導(dǎo)致占用了GC暫停時(shí)間(例如JNI編譯)。
串行活動(dòng)
- 代碼根更新 Code Root Fixup:根據(jù)轉(zhuǎn)移對(duì)象更新代碼根。
- 代碼根清理 Code Root Purge:清理代碼根集合表。
- 清除全局卡片標(biāo)記 Clear CT:在任意收集周期會(huì)掃描CSet與RSet記錄的PRT,掃描時(shí)會(huì)在全局卡片表中進(jìn)行標(biāo)記,防止重復(fù)掃描。在收集周期的最后將會(huì)清除全局卡片表中的已掃描標(biāo)志。
- 選擇下次收集集合 Choose CSet:該部分主要用于并發(fā)標(biāo)記周期后的年輕代收集、以及混合收集中,在這些收集過程中,由于有老年代候選分區(qū)的加入,往往需要對(duì)下次收集的范圍做出界定;但單純的年輕代收集中,所有收集的分區(qū)都會(huì)被收集,不存在選擇。
- 引用處理 Ref Proc:主要針對(duì)軟引用、弱引用、虛引用、final引用、JNI引用。當(dāng)Ref Proc占用時(shí)間過多時(shí),可選擇使用參數(shù)-XX:ParallelRefProcEnabled激活多線程引用處理。G1希望應(yīng)用能小心使用軟引用,因?yàn)檐浺脮?huì)一直占據(jù)內(nèi)存空間直到空間耗盡時(shí)被Full GC回收掉;即使未發(fā)生Full GC,軟引用對(duì)內(nèi)存的占用,也會(huì)導(dǎo)致GC次數(shù)的增加。
- 引用排隊(duì) Ref Enq:此項(xiàng)活動(dòng)可能會(huì)導(dǎo)致RSet的更新,此時(shí)會(huì)通過記錄日志,將關(guān)聯(lián)的卡片標(biāo)記為臟卡片。
- 卡片重新臟化 Redirty Cards:重新臟化卡片。
- 回收空閑巨型分區(qū) Humongous Reclaim:G1做了一個(gè)優(yōu)化:通過查看所有根對(duì)象以及年輕代分區(qū)的RSet,如果確定RSet中巨型對(duì)象沒有任何引用,則說明G1發(fā)現(xiàn)了一個(gè)不可達(dá)的巨型對(duì)象,該對(duì)象分區(qū)會(huì)被回收。
- 釋放分區(qū) Free CSet:回收CSet分區(qū)的所有空間,并加入到空閑分區(qū)中。
- 其他活動(dòng) Other:GC中可能還會(huì)經(jīng)歷其他耗時(shí)很小的活動(dòng),如修復(fù)JNI句柄等。
并發(fā)標(biāo)記周期后的年輕代收集 Young Collection Following Concurrent Marking Cycle
當(dāng)G1發(fā)起并發(fā)標(biāo)記周期之后,并不會(huì)馬上開始混合收集。G1會(huì)先等待下一次年輕代收集,然后在該收集階段中,確定下次混合收集的CSet(Choose CSet)。
混合收集周期 Mixed Collection Cycle
單次的混合收集與年輕代收集并無二致。根據(jù)暫停目標(biāo),老年代的分區(qū)可能不能一次暫停收集中被處理完,G1會(huì)發(fā)起連續(xù)多次的混合收集,稱為混合收集周期(Mixed Collection Cycle)。G1會(huì)計(jì)算每次加入到CSet中的分區(qū)數(shù)量、混合收集進(jìn)行次數(shù),并且在上次的年輕代收集、以及接下來的混合收集中,G1會(huì)確定下次加入CSet的分區(qū)集(Choose CSet),并且確定是否結(jié)束混合收集周期。
轉(zhuǎn)移失敗的擔(dān)保機(jī)制 Full GC
轉(zhuǎn)移失敗(Evacuation Failure)是指當(dāng)G1無法在堆空間中申請(qǐng)新的分區(qū)時(shí),G1便會(huì)觸發(fā)擔(dān)保機(jī)制,執(zhí)行一次STW式的、單線程的Full GC。Full GC會(huì)對(duì)整堆做標(biāo)記清除和壓縮,最后將只包含純粹的存活對(duì)象。參數(shù)-XX:G1ReservePercent(默認(rèn)10%)可以保留空間,來應(yīng)對(duì)晉升模式下的異常情況,最大占用整堆50%,更大也無意義。
G1在以下場(chǎng)景中會(huì)觸發(fā)Full GC,同時(shí)會(huì)在日志中記錄to-space-exhausted以及Evacuation Failure:
- 從年輕代分區(qū)拷貝存活對(duì)象時(shí),無法找到可用的空閑分區(qū)
- 從老年代分區(qū)轉(zhuǎn)移存活對(duì)象時(shí),無法找到可用的空閑分區(qū)
- 分配巨型對(duì)象時(shí)在老年代無法找到足夠的連續(xù)分區(qū)
由于G1的應(yīng)用場(chǎng)合往往堆內(nèi)存都比較大,所以Full GC的收集代價(jià)非常昂貴,應(yīng)該避免Full GC的發(fā)生
使用場(chǎng)景及優(yōu)缺點(diǎn)
根據(jù)經(jīng)驗(yàn),在大部分的大型內(nèi)存(6G以上)服務(wù)器上,無論是吞吐量還是STW時(shí)間,G1的性能都是要優(yōu)于CMS。
優(yōu)點(diǎn):并行與并發(fā)收集,分代分區(qū)收集,優(yōu)先垃圾收集,空間整合,可控或者可預(yù)測(cè)停頓時(shí)間。
缺點(diǎn):
- 收集中產(chǎn)生內(nèi)存,G1的每個(gè)region都需要有一份記憶集和卡表記錄跨代指針,這導(dǎo)致記憶集可能占用堆空間10-20%甚至更多空間。
- 執(zhí)行過程中額外負(fù)載開銷加大,寫屏障進(jìn)行維護(hù)卡表操作外,還需要原始快照能夠減少并發(fā)標(biāo)記和重新標(biāo)記階段的消耗,避免最終標(biāo)記階段停頓過長(zhǎng),運(yùn)行過程中會(huì)產(chǎn)生由跟蹤引用變化帶來的額外開銷負(fù)擔(dān),比CMS增量算法消耗更多,CMS的寫屏障實(shí)現(xiàn)直接是同步操作, 而G1是把寫屏障和寫后屏障中要做的事情放到隊(duì)列里異步處理。
- G1對(duì)于Full GC是沒有處理流程, 一旦發(fā)生Full GC G1的回收?qǐng)?zhí)行的是單線程的Serial回收器進(jìn)行回收。
注意點(diǎn)
G1一定不會(huì)產(chǎn)生內(nèi)存碎片嗎
堆內(nèi)存的動(dòng)態(tài)變化、分配模式以及回收行為等因素影響下,仍然可能出現(xiàn)一些碎片問題。當(dāng)某些Region中存在多個(gè)不連續(xù)的小塊空閑內(nèi)存,無法完全滿足某些大對(duì)象的內(nèi)存需求時(shí),仍然可以稱之為碎片問題。
- 分配模式不規(guī)律: 如果應(yīng)用程序的內(nèi)存分配模式不規(guī)律,頻繁地分配和釋放不同大小的對(duì)象,可能會(huì)導(dǎo)致一些小的空閑內(nèi)存碎片在堆中產(chǎn)生。
- 大對(duì)象分配: G1回收器的區(qū)域被劃分為不同大小的Region,當(dāng)一個(gè)大對(duì)象無法在單個(gè)Region中分配時(shí),G1可能會(huì)在多個(gè)Region中分配這個(gè)大對(duì)象,這可能導(dǎo)致跨多個(gè)Region的碎片。
- 并發(fā)情況下的內(nèi)存變化: G1回收器會(huì)在后臺(tái)進(jìn)行并發(fā)的垃圾回收,如果在回收過程中發(fā)生了內(nèi)存變化,如某個(gè)區(qū)域中的對(duì)象被回收,留下一些零散的空閑空間,也有可能會(huì)導(dǎo)致內(nèi)存碎片。
- 頻繁的Full GC: 盡管G1垃圾回收器的設(shè)計(jì)可以減少Full GC(全局垃圾回收)的頻率,但如果頻繁發(fā)生Full GC,可能會(huì)導(dǎo)致內(nèi)存布局的重組,產(chǎn)生一些碎片。