偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

G1收集器:JVM垃圾回收的新一代王者

開發(fā) 前端
堆內(nèi)存的動態(tài)變化、分配模式以及回收行為等因素影響下,仍然可能出現(xiàn)一些碎片問題。當某些Region中存在多個不連續(xù)的小塊空閑內(nèi)存,無法完全滿足某些大對象的內(nèi)存需求時,仍然可以稱之為碎片問題。

介紹

G1垃圾收集器在JDK7被開發(fā)出來,JDK8功能基本完全實現(xiàn)。并且成功替換掉了Parallel Scavenge成為了服務端模式下默認的垃圾收集器。JDK 9以后默認使用,替代了CMS 收集器。

G1和CMS一樣,也是采用三色標記分段式進行回收的算法, 不過它是寫屏障 + STAB快照實現(xiàn)。

G1 收集器的最大特點

  • G1 最大的特點是引入分區(qū)的思路,弱化了分代的概念。
  • 并行與并發(fā):G1 能充分利用 CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動作,G1 收集器仍然可以通過并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行。
  • 空間整合:與 CMS 的“標記-清除”算法不同,G1 從整體來看是基于“標記-整理”算法實現(xiàn)的收集器,不會產(chǎn)生空間碎片;從局部上來看是基于“標記-復制”算法實現(xiàn)的。
  • 可預測的停頓:G1垃圾回收器設(shè)定了用戶可控的停頓時間目標,開發(fā)者可以通過設(shè)置參數(shù)來指定允許的最大垃圾回收停頓時間。G1會根據(jù)這個目標來動態(tài)調(diào)整回收策略,盡可能地減少長時間的垃圾回收停頓。


如何完成可預測的? G1根據(jù)歷史數(shù)據(jù)來預測本次回收需要的堆分區(qū)數(shù)量,也就是選擇回收哪些內(nèi)存空間。最簡單的方法就是使用算術(shù)的平均值建立一個線性關(guān)系來進行預測。比如:過去10次一共收集了10GB的內(nèi)存,花費了1s。那么在200ms的時間下,最多可以收集2GB的內(nèi)存空間。而G1的預測邏輯是基于衰減平均值和衰減標準差來確定的。

CMS 和 G1 的區(qū)別

  • CMS 中,堆被分為 PermGen,YoungGen,OldGen ;而 YoungGen 又分了兩個 survivo 區(qū)域。在 G1 中,堆被平均分成幾個區(qū)域 (region) ,在每個區(qū)域中,雖然也保留了新老代的概念,但是收集器是以整個區(qū)域為單位收集的。
  • G1 在回收內(nèi)存后,會立即同時做合并空閑內(nèi)存的工作;而 CMS ,則默認是在 STW(stop the world)的時候做。
  • G1 會在 Young GC 中使用;而 CMS 只能在 Old 區(qū)使用

分區(qū)Region

G1同時回收新生代和老年代,但是分別被稱為G1的Young GC模式和Mixed GC模式。這個特性來源于G1獨特的內(nèi)存布局,內(nèi)存分配不再嚴格遵守新生代,老年代的劃分,而是以Region為單位,G1跟蹤各個Region的并且維護一個關(guān)于Region的優(yōu)先級列表。在合適的時機選擇合適的Region進行回收。這種基于Region的內(nèi)存劃分為一些巧妙的設(shè)計思想提供了解決停頓時間和高吞吐的基礎(chǔ)。接下來我們將詳細講解G1的詳細垃圾回收過程和里面可圈可點的設(shè)計。

圖片圖片

G1采用了分區(qū)(Region)的思路,將整個堆空間分成若干個大小相等的內(nèi)存區(qū)域,每次分配對象空間將逐段地使用內(nèi)存。因此,在堆的使用上,G1并不要求對象的存儲一定是物理上連續(xù)的,只要邏輯上連續(xù)即可;每個分區(qū)也不會確定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數(shù)-XX:G1HeapReginotallow=n可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認將整堆劃分為2048個分區(qū)

。在分代垃圾回收算法的思想下,region邏輯上劃分為Eden,Survivor和老年代。每個分區(qū)都可能是eden區(qū),Survivor區(qū)也可能是old區(qū),但在一個時刻只能是一種分區(qū)。各種角色的region個數(shù)都是不固定的,這說明每個代的內(nèi)存也是不固定的。這些region在邏輯上是連續(xù)的,而不是物理上連續(xù),這點和之前的young/old區(qū)物理連續(xù)很不一樣。

G1對內(nèi)存的使用以分區(qū)(Region)為單位

  • 堆內(nèi)存會被切分成為很多個固定大小區(qū)域(Region),每個是連續(xù)范圍的虛擬內(nèi)存。
  • 堆內(nèi)存中一個區(qū)域 (Region) 的大小,可以通過 -XX:G1HeapRegionSize 參數(shù)指定,大小區(qū)間最小 1M 、最大 32M ,總之是 2 的冪次方。
  • 默認是將堆內(nèi)存按照 2048 份均分。

圖片圖片

  • 每個 Region 被標記了 E、S、O 和 H,這些區(qū)域在邏輯上被映射為 Eden,Survivor 和老年代。存活的對象從一個區(qū)域轉(zhuǎn)移(即復制或移動)到另一個區(qū)域。區(qū)域被設(shè)計為并行收集垃圾,可能會暫停所有應用線程。如上圖所示,區(qū)域可以分配到 Eden,survivor 和老年代。
  • 巨型區(qū)域(Humongous Region):如果一個對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認為這是一個巨型對象。如果對一個短期存在的大對象使用復制算法回收的話,復制成本非常高,而直接放進old區(qū)則導致原本應該短期存在的對象占用了老年代的內(nèi)存,更加不利于回收性能。為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放巨型對象。如果一個H區(qū)裝不下一個巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。

內(nèi)部數(shù)據(jù)結(jié)構(gòu)

Card Table卡表

Card Table是Region的內(nèi)部結(jié)構(gòu)劃分。每個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時,怎么知道那些是存活對象,并且其它代區(qū)域有沒有引用這部分對象,于是把region劃分了很多card區(qū)域, 每個區(qū)域大小不超過512b,當該card區(qū)域里的對象有引用關(guān)系,將當前card置為“dirty”, 并且使用卡表(CardTable)來記錄每一塊card是否dirty,在進行GC時,不用遍歷所有的空間, 只需要遍歷卡表中為"dirty"。

圖片圖片

Rset記憶集合

除了卡表,每個region中都含有Remember Set,簡稱RSet。RSet其實是hash表,key為引用本region的其他region起始地址,value為本region中被key對應的region引用的card索引位置。

這里必須講解一下RSet存在的原因,RSet是為了解決"跨代引用"。想象一下,一個新生代對象被老年代對象引用,那么為了通過引用鏈找到這個新生代對象,從GC Roots出發(fā)遍歷對象時必須經(jīng)過老年代對象。實際上以這種方式遍歷時,是把所有對象都遍歷了一遍。但是我們的其實只想回收新生代的對象,卻把所有對象都遍歷了一遍,這無疑很低效。

在YoungGC時,當RSet存在時,順著引用鏈查找引用。如果引用鏈上出現(xiàn)了老年代對象,那么直接放棄查找這條引用鏈。當整個GC Root Tracing執(zhí)行完畢后,就知道了除被跨代引用外還存活的新生代對象。緊接著再遍歷新生代Region的RSet,如果RSet里存在key為老年代的Region,就將key對應的value代表的card的對象標記為存活,這樣就標記到了被跨代引用的新生代對象。它可以使得垃圾收集器不需要掃描整個堆去找到誰的引用了當前分區(qū)對象,是G1高效回收的關(guān)鍵點。

當然這么做會存在一個問題,如果部分老年代對象是應該被回收的對象,但還是跨代引用了新生代,會導致原本應該被回收的新生代對象躲過本輪新生代回收。這部分對象就只能等到后續(xù)的老年代的垃圾回收mixed GC來回收掉。這也是為什么G1的回收精度比較低的原因之一。

圖片圖片

以這幅圖為例,region1和region2都引用了region3中的對象,那么region3的RSet中有兩個key,分別是region1的起始地址和region2的起始地址。在掃描region3的RSet時,發(fā)現(xiàn)key為0x6a的region是一個old區(qū)region。如果這時第3,5card對應的對象沒有被標記為可達,那么這里就會根據(jù)RSet再次標記。同樣的,key為0x9b對應的region是一個young區(qū)域的region,那么0,2號card的對象則不會被標記。

事實上,并非所有的引用都需要記錄在RSet中,如果一個分區(qū)確定需要掃描,那么無需RSet也可以無遺漏的得到引用關(guān)系。那么引用源自本分區(qū)的對象,當然不用落入RSet中;同時,G1 GC每次都會對年輕代進行整體收集,因此引用源自年輕代的對象,也不需要在RSet中記錄。最后只有老年代的分區(qū)可能會有RSet記錄,這些分區(qū)稱為擁有RSet分區(qū)(an RSet’s owning region)。

Per Region Table (PRT)

RSet在內(nèi)部使用Per Region Table(PRT)記錄分區(qū)的引用情況。由于RSet的記錄要占用分區(qū)的空間,如果一個分區(qū)非常"受歡迎",那么RSet占用的空間會上升,從而降低分區(qū)的可用空間。G1應對這個問題采用了改變RSet的密度的方式,在PRT中將會以三種模式記錄引用:

  • 稀少:直接記錄引用對象的卡片索引
  • 細粒度:記錄引用對象的分區(qū)索引
  • 粗粒度:只記錄引用情況,每個分區(qū)對應一個比特位

由上可知,粗粒度的PRT只是記錄了引用數(shù)量,需要通過整堆掃描才能找出所有引用,因此掃描速度也是最慢的。

RSet和卡表的區(qū)別是什么?

卡表記錄的是堆內(nèi)存中card有沒有變成"dirty", 但是它本身不知道dirty里面哪些是引用了的對象,它是一個大維度的一個記錄,RSet是記錄自身Region中對象引用了其它Region中的那些對象,詳細的記錄對方引用對象信息,G1使用了兩者的結(jié)合,實現(xiàn)了增量式的垃圾回收,并優(yōu)化跨區(qū)引用的最終處理。詳情可以繼續(xù)看后文

堆Heap

G1同樣可以通過-Xms/-Xmx來指定堆空間大小。當發(fā)生年輕代收集或混合收集時,通過計算GC與應用的耗費時間比,自動調(diào)整堆空間大小。如果GC頻率太高,則通過增加堆尺寸,來減少GC頻率,相應地GC占用的時間也隨之降低;目標參數(shù)-XX:GCTimeRatio即為GC與應用的耗費時間比,G1默認為9,而CMS默認為99,因為CMS的設(shè)計原則是耗費在GC上的時間盡可能的少。另外,當空間不足,如對象空間分配或轉(zhuǎn)移失敗時,G1會首先嘗試增加堆空間,如果擴容失敗,則發(fā)起擔保的Full GC。Full GC后,堆尺寸計算結(jié)果也會調(diào)整堆空間。

CSet

Collection SET用于記錄可被回收分區(qū)的集合組, G1使用不同算法,動態(tài)的計算出那些分區(qū)是需要被回收的,將其放到CSet中,在CSet當中存活的數(shù)據(jù)都會在GC過程中拷貝到另一個可用分區(qū),CSet可以是所有類型分區(qū),它需要額外占用內(nèi)存,堆空間的1%。

CSet收集示意圖

圖片圖片

收集集合(CSet)代表每次GC暫停時回收的一系列目標分區(qū)。在任意一次收集暫停中,CSet所有分區(qū)都會被釋放,內(nèi)部存活的對象都會被轉(zhuǎn)移到分配的空閑分區(qū)中。因此無論是年輕代收集,還是混合收集,工作的機制都是一致的。年輕代收集CSet只容納年輕代分區(qū),而混合收集會通過啟發(fā)式算法,在老年代候選回收分區(qū)中,篩選出回收收益最高的分區(qū)添加到CSet中。

候選老年代分區(qū)的CSet準入條件,可以通過活躍度閾值-XX:G1MixedGCLiveThresholdPercent(默認85%)進行設(shè)置,從而攔截那些回收開銷巨大的對象;同時,每次混合收集可以包含候選老年代分區(qū),可根據(jù)CSet對堆的總大小占比-XX:G1OldCSetRegionThresholdPercent(默認10%)設(shè)置數(shù)量上限。

由上述可知,G1的收集都是根據(jù)CSet進行操作的,年輕代收集與混合收集沒有明顯的不同,最大的區(qū)別在于兩種收集的觸發(fā)條件。

年輕代收集集合 CSet of Young Collection

應用線程不斷活動后,年輕代空間會被逐漸填滿。當JVM分配對象到Eden區(qū)域失敗(Eden區(qū)已滿)時,便會觸發(fā)一次STW式的年輕代收集。在年輕代收集中,Eden分區(qū)存活的對象將被拷貝到Survivor分區(qū);原有Survivor分區(qū)存活的對象,將根據(jù)任期閾值(tenuring threshold)分別晉升到PLAB中,新的survivor分區(qū)和老年代分區(qū)。而原有的年輕代分區(qū)將被整體回收掉。

同時,年輕代收集還負責維護對象的年齡(存活次數(shù)),輔助判斷老化(tenuring)對象晉升的時候是到Survivor分區(qū)還是到老年代分區(qū)。年輕代收集首先先將晉升對象尺寸總和、對象年齡信息維護到年齡表中,再根據(jù)年齡表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默認50%)、最大任期閾值-XX:MaxTenuringThreshold(默認15),計算出一個恰當?shù)娜纹陂撝?,凡是超過任期閾值的對象都會被晉升到老年代。

混合收集集合 CSet of Mixed Collection

年輕代收集不斷活動后,老年代的空間也會被逐漸填充。當老年代占用空間超過整堆比IHOP閾值-XX:InitiatingHeapOccupancyPercent(默認45%)時,G1就會啟動一次混合垃圾收集周期。為了滿足暫停目標,G1可能不能一口氣將所有的候選分區(qū)收集掉,因此G1可能會產(chǎn)生連續(xù)多次的混合收集與應用線程交替執(zhí)行,每次STW的混合收集與年輕代收集過程相類似。

為了確定包含到年輕代收集集合CSet的老年代分區(qū),JVM通過參數(shù)混合周期的最大總次數(shù)-XX:G1MixedGCCountTarget(默認8)、堆廢物百分比-XX:G1HeapWastePercent(默認5%)。通過候選老年代分區(qū)總數(shù)與混合周期最大總次數(shù),確定每次包含到CSet的最小分區(qū)數(shù)量;根據(jù)堆廢物百分比,當收集達到參數(shù)時,不再啟動新的混合收集。而每次添加到CSet的分區(qū),則通過計算得到的GC效率進行安排。

Young GC流程

在了解了region的內(nèi)部結(jié)構(gòu)之后,我們再來看一下G1的young gc的具體流程。

  1. stop the world,整個young gc的流程都是在stw里進行的,這也是為什么young gc能回收全部eden區(qū)域的原因??刂苰oung gc開銷的辦法只有減少young region的個數(shù),也就是減少年輕代內(nèi)存的大小,還有就是并發(fā),多個線程同時進行g(shù)c,盡量減少stw時間。
  2. 掃描GCRoots,注意這里掃描的GC Roots就是一般意義上的GC Roots,是掃描的直接指向young代的對象,那如果GC Root是直接指向老年代對象的,則會直接停止在這一步,也就是不往下掃描了。被老年代對象指向的young代對象會在接下來的利用Rset中key指向老年代的卡表識別出來,這樣就避免了對老年代整個大的heap掃描,提高了效率。這也是為什么Rset能避免對老年代整體掃描的原因。
  3. 排空dirty card quene,更新Rset。Rset中記錄了哪些對象被老年代跨帶引用,也就是當新生代對象被老年代對象引用時,應該更新這個記錄到RSet中。但更新RSet記錄的時機不是伴隨著引用更改馬上發(fā)生的。每當老年代引用新生代對象時,這個引用記錄對應的card地址其實會被放入Dirty Card Queue(線程私有的,當線程私有的dirty card queue滿了之后會被轉(zhuǎn)移到全局的dirty card queue,這個全局是唯一的),原因是如果每次更新引用時直接更新Rset會導致多線程競爭,因為賦值操作很頻繁,影響性能。所以更新Rset交由Refinement線程來進行。全局DirtyCardQueue的容量變化分為4個階段

圖片圖片

白色:無事發(fā)生

綠色:Refinement線程被激活,-XX:G1Cnotallow=N指定的線程個數(shù)。從(全局和線程私有)隊列中拿出dirty card。并更新到對應的Rset中。

黃色:產(chǎn)生dirty card的速度過快,激活全部的Refinement線程,通過參數(shù)-XX:G1ConcRefinementYellowZnotallow=N 指定

紅色:產(chǎn)生dirty card的速度過快,將應用線程也加入到排空隊列的工作中。目的是把應用線程拖慢,減慢dirty card產(chǎn)生。

  1. 掃描Rset,掃描所有Rset中Old區(qū)到y(tǒng)oung區(qū)的引用。到這一步就確定出了young區(qū)域哪些對象是存活的。
  2. 拷貝對象到survivor區(qū)域或者晉升old區(qū)域。
  3. 處理引用隊列,軟引用,弱引用,虛引用

以上就是young gc的全部流程。

三色標記算法的漏標問題

知道了Young GC的流程后,接下來我們將學習G1針對老年代的垃圾回收過程Mixed GC,但是在正式開始介紹之前我們先講解一下可達性分析算法的具體實現(xiàn),三色標記算法。以及三色標記算法的缺陷以及G1是如何解決這個缺陷的。

在可達性分析的思想指導下,我們需要標記對象是否可達,那么我們采用將對象標記為不同的顏色來區(qū)分對象是否可達??梢岳斫馊绻粋€對象能從GC Roots出發(fā)并且遍歷到,那么對象就是可達的,這個過程我們稱為檢查。

  • 白色:對象還沒被檢查。
  • 灰色:對象被檢查了,但是對象的成員Field(對象中引用的其他對象)還沒有被檢查。這說明這個對象是可達的。
  • 黑色:對象被檢查了,對象的成員Fileld也被檢查了。

那么整個檢測的過程,就是從GC Roots出發(fā)不斷地遍歷對象,并且將可達的對象標記成黑色的過程。當標記結(jié)束時,還是白色的對象就是沒被遍歷到的對象,即不可達的對象。

舉個例子

第一輪檢查,找到所有的GC Roots,GC Roots被標記為灰色,有的GC Roots因為沒有成員Field則被標記為黑色。

圖片圖片

第二輪檢查,檢查被GC Roots引用的對象,并標記為灰色

圖片圖片

第三輪檢查,循環(huán)之前的步驟,將被標記為灰色對象的子Field檢查。因為這里就假設(shè)了3次循環(huán)檢查的對象,所以是最后一次檢查。這一路檢查結(jié)束,還是白色的對象就是可以被回收的對象。即圖例里的objectC

圖片圖片

以上描述的是一輪三色標記算法的工作過程,但是這是一個理想情況。但是在標記過程中,標記的線程是和用戶線程交替運行的,所以可能出現(xiàn)標記過程中引用發(fā)生變化的情況。

  1. 已經(jīng)存在的對象被漏標:在第二輪檢查到第三輪檢查之間,假設(shè)發(fā)生了引用的變化,objectD不再被objectB引用,而是被objectA引用,而且此時ObjectA的成員已經(jīng)被檢查完畢了,objectB的成員Field還沒被檢查。這時,objectD就永遠不會再被檢查到。這就導致了漏標。
  2. 新產(chǎn)生的對象被漏標:這個對象被已經(jīng)被標記為黑色的對象持有。比如圖例中的newObjectF。因為黑色對象已經(jīng)被認為是檢查完畢了,所以新產(chǎn)生的對象不會再被檢查,這也會導致漏標。

有兩種被漏標的情況有兩種被漏標的情況

已經(jīng)存在的對象被漏標

即圖例中被漏標的objectD,要漏標objectD,必須同時滿足:

  1. 灰色對象不再指向白色對象,即objectB.d = null
  2. 黑色對象指向白色對象,即objectA.d = objectD

要解決漏標,只要打破這兩個條件的任意一個即可。由此我們引出兩個解決方案。原始快照和增量更新。

  1. 原始快照(Snapshot At The Beginning,簡稱SATB): 當任意的灰色對象到白色對象的引用被刪除時,記錄下這個被刪除的引用,默認這個被刪除的引用對象是存活的。這也可以理解為整個檢查過程中的引用關(guān)系以檢查剛開始的那一刻為準。
  2. 增量更新(Incremental Update): 當黑色對象被新增一個白色對象的引用的時候,記錄下發(fā)生引用變更的黑色對象,并將它重新改變?yōu)榛疑珜ο?,重新標記。這是CMS采用的解決辦法

在上面的兩種解決方案里,我們發(fā)現(xiàn),無論如何,都要記錄下發(fā)生更改的引用。所以需要一種記錄引用發(fā)生更改的手段,寫屏障(write barrier)。寫屏障是一種記錄下引用發(fā)生變更的手段,效果類似AOP,但是其實現(xiàn)遠比我們使用的AOP更加底層,可以認為是在JVM代碼層面的一段代碼。每當任意的引用變更時,就會觸發(fā)這段代碼,并記錄下發(fā)生變更的引用。

新產(chǎn)生的對象被漏標

新產(chǎn)生的對象被漏標的解決方式則簡單一些,在增量更新模式下,這個問題天生就被解決了。在SATB模式下,其實是在檢查一開始就確定了一個檢查范圍,所以可以將新產(chǎn)生的對象放在檢查范圍之外,默認新產(chǎn)生的對象是存活的。當然這個過程得實際結(jié)合卡表來講解才會更加具體形象。接下來在Mixed GC的過程里再細說。

SATB

Snapshot At The Beginning,G1在分配對象時,會在region中有2個top-at-mark-start(TAMS)指針,分別表示prevTAMS和nextTAMS。對應著卡表上即指向表示卡表范圍的的兩個編號,GC是分配在nextTAMS位置以上的對象都視為活著的,這是一種隱式的標記(這涉及到G1 MixedGC垃圾回收階段的細節(jié),很復雜,接下來會詳細討論)。這種解決漏標的方式是有缺陷的,它會造成真正應該被回收的白對象躲過這次GC生存到下一次GC,這就是float garbage(浮動垃圾)。因為SATB的做法精度比較低,所以造成float garbage的情況也會比較多。

圖片圖片

為什么G1采用SATB而不用incremental update?

SATB算法:是一種基于快照的算法,它可以避免在垃圾回收時出現(xiàn)對象漏標或者重復標記的問題,從而提高垃圾回收的準確性和效率,在垃圾回收開始時,對堆中的對象引用進行快照,然后在并發(fā)標記階段中記錄下所有被修改過對象引用,保存到satb_mark_queue中,最后在重新標記階段重新掃描這些對象,標記所有被修改的對象,保證了準確性和效率。

因為采用incremental update把黑色重新標記為灰色后,之前掃描過的還要再掃描一遍,效率太低。G1有RSet與SATB相配合。Card Table里記錄了RSet,RSet里記錄了其他對象指向自己的引用,這樣就不需要再掃描其他區(qū)域,只要掃描RSet就可以了。

也就是說 灰色–>白色 引用消失時,如果沒有 黑色–>白色,引用會被push到堆棧,下次掃描時拿到這個引用,由于有RSet的存在,不需要掃描整個堆去查找指向白色的引用,效率比較高。SATB配合RSet渾然天成

Mixed GC 流程

Mixed GC從步驟上可以分為兩個大步驟,全局并發(fā)標記(global concurrent marking),拷貝存活對象(evacuation)。全局并發(fā)表的過程涉及到SATB的標記過程,我們將詳細講解。

全局并發(fā)標記(global concurrent marking)

G1收集器垃圾收集器的全局并發(fā)標記(global concurrent marking)分為多個階段

  • 初始標記(initial marking): 這個階段會STW,標記從GC Root開始直接可達的對象,這一步伴隨著young gc。之所以要young gc是為了處理跨代引用,老年代獨享也可能被年輕代跨代引用,但是老年代不能使用RSet來解決跨代引用。還有就是young gc也會stw,在第一步y(tǒng)oung gc可以共用stw的時間,盡量減少stw時間。這一步還初始化了一些參數(shù),將bottom指針賦值給prevTAMS指針,top指針賦值給nextTAMS指針,同時清空nextBitMap指針。因為之后的并發(fā)標記需要使用到這三個變量。

top,prevTAMS,nextTAMS,top都是指向卡表的指針,他們的存在是為了標識哪些對象是可以被回收的,哪些是存活的,這就是SATB機制。而nextBitMap則是記錄下卡表中哪些對象是存活的一個數(shù)組,當然現(xiàn)在還沒開始檢查,nextBitMap里的記錄都是空。

圖片圖片

  • 根分區(qū)掃描(root region scan): 這個階段在stw之后,會掃描survivor區(qū)域(survivor分區(qū)就是根分區(qū)),將所有被survivor區(qū)域?qū)ο笠玫睦夏甏鷮ο髽擞?。這也是上一步需要young gc的原因,處理跨代引用時需要知道哪些old區(qū)對象被S區(qū)對象引用。這個過程因為需要掃描survivor分區(qū),所以不能發(fā)生young gc,如果掃描過程中新生代被耗盡,那么必須等待掃描結(jié)束才可以開始young gc。這一步耗時很短。
  • 并發(fā)標記(Concurrent Marking) 從GC Roots開始對堆中對象進行可達性分析,找出各個region的存活對象信息,耗時較長。粗略過程是這樣的,但實際這一步的過程很復雜。因為要考慮在SATB機制之下,各個指針的變化。 假設(shè)在根分區(qū)掃描后沒有引用的改變,那么一個region的分區(qū)狀態(tài)和第一步init marking初始化完一致。此時如果再繼續(xù)分配對象,那么對象會分配在nextTAMS之后,隨著對象的分配,TOP指針會向后移動。

圖片圖片

因為這一步是和mutator(用戶線程)并發(fā)運行的,所以從根節(jié)點掃描的時候其實是掃描的一個快照snapshot,快照位置就是prevTAMS到nextTAMS(注意快照位置是不變的,但是prevTAMS到nextTAMS之間的對象在掃描過程中會改變)。 當region中分配新對象時,新對象都會分配在nextTAMS之后,這導致top指向的位置也往后移動,nextTAMS和top之間選哪個都是被認為隱式存活。 還有這期間也有可能應該被掃描的位置prevTAMS和nextTAMS之間的位置引用發(fā)生了變化,比如白色對象被黑色對象持有了,這就是三色標記算法的缺陷,需要更改白色對象的狀態(tài)。這里會將引用被更改的對象放入satb_mark_queue。satb_mark_queue是一個隊列,里面記錄所有被改變引用關(guān)系的白色對象。這里指的satb_mark_queue指的全局的queue。除了全局的queue,每個線程也有自己的satb mark queue,全局的queue的引用是由所有其他線程的satb mark queue合并得來的,線程的satb mark queu滿了會被轉(zhuǎn)移到全局satb mark queue。且并發(fā)標記階段會定期檢查全局satb mark queue的容量,超過某個容量就concurrent marker線程就會將全局satb mark que和線程satb mark que的對象都取出來全部標記上,當然也會將這些對象的子field全部壓棧(marking stack)等待接下來被標記到,這個處理類似于全局dirty card quene。這里注意。

圖片圖片

隨著并發(fā)標記結(jié)束nextBitMap里也標記了哪些對象是可以回收的,但注意,不一定每個線程里satb mark queue都被轉(zhuǎn)移到了全局的satb mark queue,因為合并這個過程也是并發(fā)的。所以需要下一步

  • 最終標記(remark): 標記那些并發(fā)標記階段發(fā)生變化的對象,就是將線程satb mark queue中引用發(fā)生更改的對象找出來,放入satb mark queue。這個階段為了保證標記正確必須STW。
  • 清點垃圾(cleanup): 對各個region的回收價值和成本進行排序,根據(jù)用戶期待的GC停頓時間指定回收計劃,選中部分old region,和全部的young region,這些被選中的分區(qū)稱為Collection Set(Cset),還會把沒有任何對象的region加入到可用來分配對象的region集合中。注意這一步不是清除,是清點出哪些region值得回收,不會復制任何對象。清點執(zhí)行完,一個全局并發(fā)標記周期基本就執(zhí)行完了。這時還會將nextTAMS指針賦值給prevTAMS,且nextBitMap賦值給prevBitMap。

這里是不是很奇怪為什么要記錄本輪標記的結(jié)果到prevBitMap,難道下次再來檢查本region時還可以再復用這個標記結(jié)果嗎。 我們知道G1是可以根據(jù)內(nèi)存的變化自己調(diào)整內(nèi)存中E區(qū),O區(qū)的容量的,如果其中某些分區(qū)容量增長比較快,說明這個分區(qū)的內(nèi)存訪問更頻繁,在未來也可能更快地達到region的容量限制,那么下次復制轉(zhuǎn)移時就會優(yōu)先將這塊region中的對象轉(zhuǎn)移到更大的region中去。

拷貝存活對象evacuation

標記結(jié)束剩下的就是轉(zhuǎn)移evacuation,拷貝存活對象。就是將活著的對象拷貝到空的region,再回收掉部分region。這一步是采用多線程復制清除,整個過程會STW。這也是G1的優(yōu)勢之一,只要還有一塊空閑的region,就可以完成垃圾回收。而不用像CMS那樣必須預留太多的內(nèi)存。

G1 的活動周期

G1垃圾收集活動匯總

圖片圖片

RSet的維護

由于不能整堆掃描,又需要計算分區(qū)確切的活躍度,因此,G1需要一個增量式的完全標記并發(fā)算法,通過維護RSet,得到準確的分區(qū)引用信息。在G1中,RSet的維護主要來源兩個方面:寫柵欄(Write Barrier)和并發(fā)優(yōu)化線程(Concurrence Refinement Threads)

柵欄Barrier

柵欄代碼示意

圖片圖片

柵欄是指在原生代碼片段中,當某些語句被執(zhí)行時,柵欄代碼也會被執(zhí)行。而G1主要在賦值語句中,使用寫前柵欄(Pre-Write Barrrier)和寫后柵欄(Post-Write Barrrier)。事實上,寫柵欄的指令序列開銷非常昂貴,應用吞吐量也會根據(jù)柵欄復雜度而降低。

寫前柵欄 Pre-Write Barrrier

即將執(zhí)行一段賦值語句時,等式左側(cè)對象將修改引用到另一個對象,那么等式左側(cè)對象原先引用的對象所在分區(qū)將因此喪失一個引用,那么JVM就需要在賦值語句生效之前,記錄喪失引用的對象。JVM并不會立即維護RSet,而是通過批量處理,在將來RSet更新(見SATB)。

寫后柵欄 Post-Write Barrrier

當執(zhí)行一段賦值語句后,等式右側(cè)對象獲取了左側(cè)對象的引用,那么等式右側(cè)對象所在分區(qū)的RSet也應該得到更新。同樣為了降低開銷,寫后柵欄發(fā)生后,RSet也不會立即更新,同樣只是記錄此次更新日志,在將來批量處理(見Concurrence Refinement Threads)。

起始快照算法Snapshot at the beginning (SATB)

Taiichi Tuasa貢獻的增量式完全并發(fā)標記算法起始快照算法(SATB),主要針對標記-清除垃圾收集器的并發(fā)標記階段,非常適合G1的分區(qū)塊的堆結(jié)構(gòu),同時解決了CMS的主要煩惱:重新標記暫停時間長帶來的潛在風險。

SATB會創(chuàng)建一個對象圖,相當于堆的邏輯快照,從而確保并發(fā)標記階段所有的垃圾對象都能通過快照被鑒別出來。當賦值語句發(fā)生時,應用將會改變了它的對象圖,那么JVM需要記錄被覆蓋的對象。因此寫前柵欄會在引用變更前,將值記錄在SATB日志或緩沖區(qū)中。每個線程都會獨占一個SATB緩沖區(qū),初始有256條記錄空間。當空間用盡時,線程會分配新的SATB緩沖區(qū)繼續(xù)使用,而原有的緩沖去則加入全局列表中。最終在并發(fā)標記階段,并發(fā)標記線程(Concurrent Marking Threads)在標記的同時,還會定期檢查和處理全局緩沖區(qū)列表的記錄,然后根據(jù)標記位圖分片的標記位,掃描引用字段來更新RSet。此過程又稱為并發(fā)標記/SATB寫前柵欄。

并發(fā)優(yōu)化線程Concurrence Refinement Threads

G1中使用基于Urs H?lzle的快速寫柵欄,將柵欄開銷縮減到2個額外的指令。柵欄將會更新一個card table type的結(jié)構(gòu)來跟蹤代間引用。

當賦值語句發(fā)生后,寫后柵欄會先通過G1的過濾技術(shù)判斷是否是跨分區(qū)的引用更新,并將跨分區(qū)更新對象的卡片加入緩沖區(qū)序列,即更新日志緩沖區(qū)或臟卡片隊列。與SATB類似,一旦日志緩沖區(qū)用盡,則分配一個新的日志緩沖區(qū),并將原來的緩沖區(qū)加入全局列表中。

并發(fā)優(yōu)化線程(Concurrence Refinement Threads),只專注掃描日志緩沖區(qū)記錄的卡片來維護更新RSet,線程最大數(shù)目可通過-XX:G1ConcRefinementThreads(默認等于-XX:ParellelGCThreads)設(shè)置。并發(fā)優(yōu)化線程永遠是活躍的,一旦發(fā)現(xiàn)全局列表有記錄存在,就開始并發(fā)處理。如果記錄增長很快或者來不及處理,那么通過閾值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1會用分層的方式調(diào)度,使更多的線程處理全局列表。如果并發(fā)優(yōu)化線程也不能跟上緩沖區(qū)數(shù)量,則Mutator線程(Java應用線程)會掛起應用并被加進來幫助處理,直到全部處理完。因此,必須避免此類場景出現(xiàn)。

并發(fā)標記周期 Concurrent Marking Cycle

并發(fā)標記周期是G1中非常重要的階段,這個階段將會為混合收集周期識別垃圾最多的老年代分區(qū)。整個周期完成根標記、識別所有(可能)存活對象,并計算每個分區(qū)的活躍度,從而確定GC效率等級。

當達到IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認45%)時,便會觸發(fā)并發(fā)標記周期。整個并發(fā)標記周期將由初始標記(Initial Mark)、根分區(qū)掃描(Root Region Scanning)、并發(fā)標記(Concurrent Marking)、重新標記(Remark)、清除(Cleanup)幾個階段組成。其中,初始標記(隨年輕代收集一起活動)、重新標記、清除是STW的,而并發(fā)標記如果來不及標記存活對象,則可能在并發(fā)標記過程中,G1又觸發(fā)了幾次年輕代收集。

并發(fā)標記線程 Concurrent Marking Threads

并發(fā)標記位圖過程

圖片圖片

要標記存活的對象,每個分區(qū)都需要創(chuàng)建位圖(Bitmap)信息來存儲標記數(shù)據(jù),來確定標記周期內(nèi)被分配的對象。G1采用了兩個位圖Previous Bitmap、Next Bitmap,來存儲標記數(shù)據(jù),Previous位圖存儲上次的標記數(shù)據(jù),Next位圖在標記周期內(nèi)不斷變化更新,同時Previous位圖的標記數(shù)據(jù)也越來越過時,當標記周期結(jié)束后Next位圖便替換Previous位圖,成為上次標記的位圖。同時,每個分區(qū)通過頂部開始標記(TAMS),來記錄已標記過的內(nèi)存范圍。同樣的,G1使用了兩個頂部開始標記Previous TAMS(PTAMS)、Next TAMS(NTAMS),記錄已標記的范圍。

在并發(fā)標記階段,G1會根據(jù)參數(shù)-XX:ConcGCThreads(默認GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4),分配并發(fā)標記線程(Concurrent Marking Threads),進行標記活動。每個并發(fā)線程一次只掃描一個分區(qū),并通過"手指"指針的方式優(yōu)化獲取分區(qū)。并發(fā)標記線程是爆發(fā)式的,在給定的時間段拼命干活,然后休息一段時間,再拼命干活。

每個并發(fā)標記周期,在初始標記STW的最后,G1會分配一個空的Next位圖和一個指向分區(qū)頂部(Top)的NTAMS標記。Previous位圖記錄的上次標記數(shù)據(jù),上次的標記位置,即PTAMS,在PTAMS與分區(qū)底部(Bottom)的范圍內(nèi),所有的存活對象都已被標記。那么,在PTAMS與Top之間的對象都將是隱式存活(Implicitly Live)對象。在并發(fā)標記階段,Next位圖吸收了Previous位圖的標記數(shù)據(jù),同時每個分區(qū)都會有新的對象分配,則Top與NTAMS分離,前往更高的地址空間。在并發(fā)標記的一次標記中,并發(fā)標記線程將找出NTAMS與PTAMS之間的所有存活對象,將標記數(shù)據(jù)存儲在Next位圖中。同時,在NTAMS與Top之間的對象即成為已標記對象。如此不斷地更新Next位圖信息,并在清除階段與Previous位圖交換角色。

初始標記 Initial Mark

初始標記(Initial Mark)負責標記所有能被直接可達的根對象(原生棧對象、全局對象、JNI對象),根是對象圖的起點,因此初始標記需要將Mutator線程(Java應用線程)暫停掉,也就是需要一個STW的時間段。事實上,當達到IHOP閾值時,G1并不會立即發(fā)起并發(fā)標記周期,而是等待下一次年輕代收集,利用年輕代收集的STW時間段,完成初始標記,這種方式稱為借道(Piggybacking)。在初始標記暫停中,分區(qū)的NTAMS都被設(shè)置到分區(qū)頂部Top,初始標記是并發(fā)執(zhí)行,直到所有的分區(qū)處理完。

根分區(qū)掃描 Root Region Scanning

在初始標記暫停結(jié)束后,年輕代收集也完成的對象復制到Survivor的工作,應用線程開始活躍起來。此時為了保證標記算法的正確性,所有新復制到Survivor分區(qū)的對象,都需要被掃描并標記成根,這個過程稱為根分區(qū)掃描(Root Region Scanning),同時掃描的Suvivor分區(qū)也被稱為根分區(qū)(Root Region)。根分區(qū)掃描必須在下一次年輕代垃圾收集啟動前完成(并發(fā)標記的過程中,可能會被若干次年輕代垃圾收集打斷),因為每次GC會產(chǎn)生新的存活對象集合。

并發(fā)標記 Concurrent Marking

和應用線程并發(fā)執(zhí)行,并發(fā)標記線程在并發(fā)標記階段啟動,由參數(shù)-XX:ConcGCThreads(默認GC線程數(shù)的1/4,即-XX:ParallelGCThreads/4)控制啟動數(shù)量,每個線程每次只掃描一個分區(qū),從而標記出存活對象圖。在這一階段會處理Previous/Next標記位圖,掃描標記對象的引用字段。同時,并發(fā)標記線程還會定期檢查和處理STAB全局緩沖區(qū)列表的記錄,更新對象引用信息。參數(shù)-XX:+ClassUnloadingWithConcurrentMark會開啟一個優(yōu)化,如果一個類不可達(不是對象不可達),則在重新標記階段,這個類就會被直接卸載。所有的標記任務必須在堆滿前就完成掃描,如果并發(fā)標記耗時很長,那么有可能在并發(fā)標記過程中,又經(jīng)歷了幾次年輕代收集。如果堆滿前沒有完成標記任務,則會觸發(fā)擔保機制,經(jīng)歷一次長時間的串行Full GC。

存活數(shù)據(jù)計算 Live Data Accounting

存活數(shù)據(jù)計算(Live Data Accounting)是標記操作的附加產(chǎn)物,只要一個對象被標記,同時會被計算字節(jié)數(shù),并計入分區(qū)空間。只有NTAMS以下的對象會被標記和計算,在標記周期的最后,Next位圖將被清空,等待下次標記周期。

重新標記 Remark

重新標記(Remark)是最后一個標記階段。在該階段中,G1需要一個暫停的時間,去處理剩下的SATB日志緩沖區(qū)和所有更新,找出所有未被訪問的存活對象,同時安全完成存活數(shù)據(jù)計算。這個階段也是并行執(zhí)行的,通過參數(shù)-XX:ParallelGCThread可設(shè)置GC暫停時可用的GC線程數(shù)。同時,引用處理也是重新標記階段的一部分,所有重度使用引用對象(弱引用、軟引用、虛引用、最終引用)的應用都會在引用處理上產(chǎn)生開銷。

清除 Cleanup

緊挨著重新標記階段的清除(Clean)階段也是STW的。Previous/Next標記位圖、以及PTAMS/NTAMS,都會在清除階段交換角色。清除階段主要執(zhí)行以下操作:

  • RSet梳理,啟發(fā)式算法會根據(jù)活躍度和RSet尺寸對分區(qū)定義不同等級,同時RSet數(shù)理也有助于發(fā)現(xiàn)無用的引用。參數(shù)-XX:+PrintAdaptiveSizePolicy可以開啟打印啟發(fā)式算法決策細節(jié);
  • 整理堆分區(qū),為混合收集周期識別回收收益高(基于釋放空間和暫停目標)的老年代分區(qū)集合;
  • 識別所有空閑分區(qū),即發(fā)現(xiàn)無存活對象的分區(qū)。該分區(qū)可在清除階段直接回收,無需等待下次收集周期。

年輕代收集/混合收集周期

年輕代收集和混合收集周期,是G1回收空間的主要活動。當應用運行開始時,堆內(nèi)存可用空間還比較大,只會在年輕代滿時,觸發(fā)年輕代收集;隨著老年代內(nèi)存增長,當?shù)竭_IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認45%)時,G1開始著手準備收集老年代空間。首先經(jīng)歷并發(fā)標記周期,識別出高收益的老年代分區(qū),前文已述。但隨后G1并不會馬上開始一次混合收集,而是讓應用線程先運行一段時間,等待觸發(fā)一次年輕代收集。在這次STW中,G1將保準整理混合收集周期。接著再次讓應用線程運行,當接下來的幾次年輕代收集時,將會有老年代分區(qū)加入到CSet中,即觸發(fā)混合收集,這些連續(xù)多次的混合收集稱為混合收集周期(Mixed Collection Cycle)。

GC工作線程數(shù)

GC工作線程數(shù) -XX:ParallelGCThreads

JVM可以通過參數(shù)-XX:ParallelGCThreads進行指定GC工作的線程數(shù)量。參數(shù)-XX:ParallelGCThreads默認值并不是固定的,而是根據(jù)當前的CPU資源進行計算。如果用戶沒有指定,且CPU小于等于8,則默認與CPU核數(shù)相等;若CPU大于8,則默認JVM會經(jīng)過計算得到一個小于CPU核數(shù)的線程數(shù);當然也可以人工指定與CPU核數(shù)相等。

年輕代收集 Young Collection

每次收集過程中,既有并行執(zhí)行的活動,也有串行執(zhí)行的活動,但都可以是多線程的。在并行執(zhí)行的任務中,如果某個任務過重,會導致其他線程在等待某項任務的處理,需要對這些地方進行優(yōu)化。

并行活動

  • 外部根分區(qū)掃描 Ext Root Scanning:此活動對堆外的根(JVM系統(tǒng)目錄、VM數(shù)據(jù)結(jié)構(gòu)、JNI線程句柄、硬件寄存器、全局變量、線程對棧根)進行掃描,發(fā)現(xiàn)那些沒有加入到暫停收集集合CSet中的對象。如果系統(tǒng)目錄(單根)擁有大量加載的類,最終可能其他并行活動結(jié)束后,該活動依然沒有結(jié)束而帶來的等待時間。
  • 更新已記憶集合 Update RS:并發(fā)優(yōu)化線程會對臟卡片的分區(qū)進行掃描更新日志緩沖區(qū)來更新RSet,但只會處理全局緩沖列表。作為補充,所有被記錄但是還沒有被優(yōu)化線程處理的剩余緩沖區(qū),會在該階段處理,變成已處理緩沖區(qū)(Processed Buffers)。為了限制花在更新RSet的時間,可以設(shè)置暫停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默認10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志緩沖區(qū)更新的任務不降低,單純地減少RSet的更新時間,會導致暫停中被處理的緩沖區(qū)減少,將日志緩沖區(qū)更新工作推到并發(fā)優(yōu)化線程上,從而增加對Java應用線程資源的爭奪。
  • RSet掃描 Scan RS:在收集當前CSet之前,考慮到分區(qū)外的引用,必須掃描CSet分區(qū)的RSet。如果RSet發(fā)生粗化,則會增加RSet的掃描時間。開啟診斷模式-XX:UnlockDiagnosticVMOptions后,通過參數(shù)-XX:+G1SummarizeRSetStats可以確定并發(fā)優(yōu)化線程是否能夠及時處理更新日志緩沖區(qū),并提供更多的信息,來幫助為RSet粗化總數(shù)提供窗口。參數(shù)-XX:G1SummarizeRSetStatsPeriod=n可設(shè)置RSet的統(tǒng)計周期,即經(jīng)歷多少此GC后進行一次統(tǒng)計
  • 代碼根掃描 Code Root Scanning:對代碼根集合進行掃描,掃描JVM編譯后代碼Native Method的引用信息(nmethod掃描),進行RSet掃描。事實上,只有CSet分區(qū)中的RSet有強代碼根時,才會做nmethod掃描,查找對CSet的引用。
  • 轉(zhuǎn)移和回收 Object Copy:通過選定的CSet以及CSet分區(qū)完整的引用集,將執(zhí)行暫停時間的主要部分:CSet分區(qū)存活對象的轉(zhuǎn)移、CSet分區(qū)空間的回收。通過工作竊取機制來負載均衡地選定復制對象的線程,并且復制和掃描對象被轉(zhuǎn)移的存活對象將拷貝到每個GC線程分配緩沖區(qū)GCLAB。G1會通過計算,預測分區(qū)復制所花費的時間,從而調(diào)整年輕代的尺寸。
  • 終止 Termination:完成上述任務后,如果任務隊列已空,則工作線程會發(fā)起終止要求。如果還有其他線程繼續(xù)工作,空閑的線程會通過工作竊取機制嘗試幫助其他線程處理。而單獨執(zhí)行根分區(qū)掃描的線程,如果任務過重,最終會晚于終止。
  • GC外部的并行活動 GC Worker Other:該部分并非GC的活動,而是JVM的活動導致占用了GC暫停時間(例如JNI編譯)。

串行活動

  • 代碼根更新 Code Root Fixup:根據(jù)轉(zhuǎn)移對象更新代碼根。
  • 代碼根清理 Code Root Purge:清理代碼根集合表。
  • 清除全局卡片標記 Clear CT:在任意收集周期會掃描CSet與RSet記錄的PRT,掃描時會在全局卡片表中進行標記,防止重復掃描。在收集周期的最后將會清除全局卡片表中的已掃描標志。
  • 選擇下次收集集合 Choose CSet:該部分主要用于并發(fā)標記周期后的年輕代收集、以及混合收集中,在這些收集過程中,由于有老年代候選分區(qū)的加入,往往需要對下次收集的范圍做出界定;但單純的年輕代收集中,所有收集的分區(qū)都會被收集,不存在選擇。
  • 引用處理 Ref Proc:主要針對軟引用、弱引用、虛引用、final引用、JNI引用。當Ref Proc占用時間過多時,可選擇使用參數(shù)-XX:ParallelRefProcEnabled激活多線程引用處理。G1希望應用能小心使用軟引用,因為軟引用會一直占據(jù)內(nèi)存空間直到空間耗盡時被Full GC回收掉;即使未發(fā)生Full GC,軟引用對內(nèi)存的占用,也會導致GC次數(shù)的增加。
  • 引用排隊 Ref Enq:此項活動可能會導致RSet的更新,此時會通過記錄日志,將關(guān)聯(lián)的卡片標記為臟卡片。
  • 卡片重新臟化 Redirty Cards:重新臟化卡片。
  • 回收空閑巨型分區(qū) Humongous Reclaim:G1做了一個優(yōu)化:通過查看所有根對象以及年輕代分區(qū)的RSet,如果確定RSet中巨型對象沒有任何引用,則說明G1發(fā)現(xiàn)了一個不可達的巨型對象,該對象分區(qū)會被回收。
  • 釋放分區(qū) Free CSet:回收CSet分區(qū)的所有空間,并加入到空閑分區(qū)中。
  • 其他活動 Other:GC中可能還會經(jīng)歷其他耗時很小的活動,如修復JNI句柄等。

并發(fā)標記周期后的年輕代收集 Young Collection Following Concurrent Marking Cycle

當G1發(fā)起并發(fā)標記周期之后,并不會馬上開始混合收集。G1會先等待下一次年輕代收集,然后在該收集階段中,確定下次混合收集的CSet(Choose CSet)。

混合收集周期 Mixed Collection Cycle

單次的混合收集與年輕代收集并無二致。根據(jù)暫停目標,老年代的分區(qū)可能不能一次暫停收集中被處理完,G1會發(fā)起連續(xù)多次的混合收集,稱為混合收集周期(Mixed Collection Cycle)。G1會計算每次加入到CSet中的分區(qū)數(shù)量、混合收集進行次數(shù),并且在上次的年輕代收集、以及接下來的混合收集中,G1會確定下次加入CSet的分區(qū)集(Choose CSet),并且確定是否結(jié)束混合收集周期。

轉(zhuǎn)移失敗的擔保機制 Full GC

轉(zhuǎn)移失敗(Evacuation Failure)是指當G1無法在堆空間中申請新的分區(qū)時,G1便會觸發(fā)擔保機制,執(zhí)行一次STW式的、單線程的Full GC。Full GC會對整堆做標記清除和壓縮,最后將只包含純粹的存活對象。參數(shù)-XX:G1ReservePercent(默認10%)可以保留空間,來應對晉升模式下的異常情況,最大占用整堆50%,更大也無意義。

G1在以下場景中會觸發(fā)Full GC,同時會在日志中記錄to-space-exhausted以及Evacuation Failure:

  • 從年輕代分區(qū)拷貝存活對象時,無法找到可用的空閑分區(qū)
  • 從老年代分區(qū)轉(zhuǎn)移存活對象時,無法找到可用的空閑分區(qū)
  • 分配巨型對象時在老年代無法找到足夠的連續(xù)分區(qū)

由于G1的應用場合往往堆內(nèi)存都比較大,所以Full GC的收集代價非常昂貴,應該避免Full GC的發(fā)生

使用場景及優(yōu)缺點

根據(jù)經(jīng)驗,在大部分的大型內(nèi)存(6G以上)服務器上,無論是吞吐量還是STW時間,G1的性能都是要優(yōu)于CMS。

優(yōu)點:并行與并發(fā)收集,分代分區(qū)收集,優(yōu)先垃圾收集,空間整合,可控或者可預測停頓時間。

缺點:

  • 收集中產(chǎn)生內(nèi)存,G1的每個region都需要有一份記憶集和卡表記錄跨代指針,這導致記憶集可能占用堆空間10-20%甚至更多空間。
  • 執(zhí)行過程中額外負載開銷加大,寫屏障進行維護卡表操作外,還需要原始快照能夠減少并發(fā)標記和重新標記階段的消耗,避免最終標記階段停頓過長,運行過程中會產(chǎn)生由跟蹤引用變化帶來的額外開銷負擔,比CMS增量算法消耗更多,CMS的寫屏障實現(xiàn)直接是同步操作, 而G1是把寫屏障和寫后屏障中要做的事情放到隊列里異步處理。
  • G1對于Full GC是沒有處理流程, 一旦發(fā)生Full GC G1的回收執(zhí)行的是單線程的Serial回收器進行回收。

注意點

G1一定不會產(chǎn)生內(nèi)存碎片嗎

堆內(nèi)存的動態(tài)變化、分配模式以及回收行為等因素影響下,仍然可能出現(xiàn)一些碎片問題。當某些Region中存在多個不連續(xù)的小塊空閑內(nèi)存,無法完全滿足某些大對象的內(nèi)存需求時,仍然可以稱之為碎片問題。

  1. 分配模式不規(guī)律: 如果應用程序的內(nèi)存分配模式不規(guī)律,頻繁地分配和釋放不同大小的對象,可能會導致一些小的空閑內(nèi)存碎片在堆中產(chǎn)生。
  2. 大對象分配: G1回收器的區(qū)域被劃分為不同大小的Region,當一個大對象無法在單個Region中分配時,G1可能會在多個Region中分配這個大對象,這可能導致跨多個Region的碎片。
  3. 并發(fā)情況下的內(nèi)存變化: G1回收器會在后臺進行并發(fā)的垃圾回收,如果在回收過程中發(fā)生了內(nèi)存變化,如某個區(qū)域中的對象被回收,留下一些零散的空閑空間,也有可能會導致內(nèi)存碎片。
  4. 頻繁的Full GC: 盡管G1垃圾回收器的設(shè)計可以減少Full GC(全局垃圾回收)的頻率,但如果頻繁發(fā)生Full GC,可能會導致內(nèi)存布局的重組,產(chǎn)生一些碎片。
責任編輯:武曉燕 來源: SevenCoding
相關(guān)推薦

2010-09-26 13:29:46

JVM垃圾回收

2023-11-16 08:00:56

Java11G1

2025-05-16 08:00:00

2012-01-10 14:25:36

JavaJVM

2021-08-15 18:59:13

垃圾收集器JDK

2020-08-07 14:05:02

垃圾回收器ZGC

2015-06-17 14:10:52

OracleJava 9垃圾收集器

2009-08-14 08:56:49

Java垃圾回收器G1

2015-07-29 10:28:59

JVM參數(shù)配置參數(shù)

2009-07-24 09:41:45

Java 7 G1垃圾回收器

2024-08-26 08:58:50

2024-12-30 08:03:08

2017-09-21 14:40:06

jvm算法收集器

2022-02-25 08:01:34

CMS

2024-10-22 16:26:11

2010-05-12 18:23:21

新一代數(shù)據(jù)中心H3C

2024-12-03 09:01:33

2021-11-05 15:23:20

JVM回收算法

2022-04-19 11:25:31

JVMZGC垃圾收集器

2022-03-21 11:33:11

JVM垃圾回收器垃圾回收算法
點贊
收藏

51CTO技術(shù)棧公眾號