在前一篇的文章《HotSpot垃圾回收算法概述》里面,對于Serial, Parallel和CMS幾種垃圾回收器做了比較詳細的描述。但是對于G1的敘述是比較粗糙的。這篇文章則是提供了G1垃圾回收器的詳細分析。
概述
G1垃圾回收器是在Java7 update 4之后引入的一個新的垃圾回收器。G1是一個分代的,增量的,并行與并發(fā)的標記-復(fù)制垃圾回收器。它的設(shè)計目標是為了適應(yīng)現(xiàn)在不斷擴大的內(nèi)存和不斷增加的處理器數(shù)量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量。G1回收器和CMS比起來,有以下不同:
- G1垃圾回收器是compacting的,因此其回收得到的空間是連續(xù)的。這避免了CMS回收器因為不連續(xù)空間所造成的問題。如需要更大的堆空間,更多的floating garbage。連續(xù)空間意味著G1垃圾回收器可以不必采用空閑鏈表的內(nèi)存分配方式,而可以直接采用bump-the-pointer的方式;
- G1回收器的內(nèi)存與CMS回收器要求的內(nèi)存模型有極大的不同。G1將內(nèi)存劃分一個個固定大小的region,每個region可以是年輕代、老年代的一個。內(nèi)存的回收是以region作為基本單位的;
G1還有一個及其重要的特性:軟實時(soft real-time)。所謂的實時垃圾回收,是指在要求的時間內(nèi)完成垃圾回收。“軟實時”則是指,用戶可以指定垃圾回收時間的限時,G1會努力在這個時限內(nèi)完成垃圾回收,但是G1并不擔(dān)保每次都能在這個時限內(nèi)完成垃圾回收。通過設(shè)定一個合理的目標,可以讓達到90%以上的垃圾回收時間都在這個時限內(nèi)。
數(shù)據(jù)結(jié)構(gòu)
在了解G1垃圾回收器的算法前,先熟悉在G1中使用的一些數(shù)據(jù)結(jié)構(gòu)和概念是有用的。如果只是想大概了解一下G1垃圾回收器,那么此節(jié)可以跳過。即便想詳細了解G1回收器的細節(jié)也可以先跳過這一節(jié),直接閱讀下面的算法詳解,遇到了不明白的數(shù)據(jù)結(jié)構(gòu)和概念,再回來此處尋找解釋。
G1垃圾回收器的復(fù)雜難懂,有很大一部分原因是因為這些數(shù)據(jù)結(jié)構(gòu)。
Heap Region
本質(zhì)上來說,G1垃圾回收器依然是一個分代垃圾回收器。但是它與一般的回收器所不同的是,它引入了額外的概念,Region。G1垃圾回收器把堆劃分成一個個大小相同的Region。在HotSpot的實現(xiàn)中,整個堆被劃分成2048左右個Region。每個Region的大小在1-32MB之間,具體多大取決于堆的大小。
G1垃圾回收器的分代也是建立在這些Region的基礎(chǔ)上的。對于Region來說,它會有一個分代的類型,并且是唯一一個。即,每一個Region,它要么是young的,要么是old的。還有一類十分特殊的Humongous。所謂的Humongous,就是一個對象的大小超過了某一個閾值——HotSpot中是Region的1/2,那么它會被標記為Humongous。如果我們審視HotSpot的其余的垃圾回收器,可以發(fā)現(xiàn)這種對象以前被稱為大對象,會被直接分配老年代。而在G1回收器中,則是做了特殊的處理。
G1并不要求相同類型的region要相鄰。換言之,就是G1回收器不要求它們連續(xù)。當然在邏輯上,分代依舊是連續(xù)的。因此,一種典型的分配可能是:
注:圖片來自G1: One Garbage Collector To Rule Them All
其中E代表的是Eden,S代表的是Survivor,H代表的是Humongous,剩余的深藍色代表的是Old(或者Tenured),灰色的代表的是空閑的region。
每一個分配的Region,都可以分成兩個部分,已分配的和未被分配的。它們之間的界限被稱為top。總體上來說,把一個對象分配到Region內(nèi),只需要簡單增加top的值。這個做法實際上就是bump-the-pointer。過程如下:
Region可以說是G1回收器一次回收的最小單元。即每一次回收都是回收N個Region。這個N是多少,主要受到G1回收的效率和用戶設(shè)置的軟實時目標有關(guān)。每一次的回收,G1會選擇可能回收最多垃圾的Region進行回收。與此同時,G1回收器會維護一個空間Region的鏈表。每次回收之后的Region都會被加入到這個鏈表中。
每一次都只有一個Region處于被分配的狀態(tài)中,被稱為current region。在多線程的情況下,這會帶來并發(fā)的問題。G1回收器采用和CMS一樣的TLABs的手段。即為每一個線程分配一個Buffer,線程分配內(nèi)存就在這個Buffer內(nèi)分配。但是當線程耗盡了自己的Buffer之后,需要申請新的Buffer。這個時候依然會帶來并發(fā)的問題。G1回收器采用的是CAS(Compate And Swap)操作。
為線程分配Buffer的過程大概是:
- 記錄top值;
- 準備分配;
- 比較記錄的top值和現(xiàn)在的top值,如果一樣,則執(zhí)行分配,并且更新top的值;否則,重復(fù)1;
顯然的,采用TLABs的技術(shù),就會帶來碎片。舉例來說,當一個線程在自己的Buffer里面分配的時候,雖然Buffer里面還有剩余的空間,但是卻因為分配的對象過大以至于這些空閑空間無法容納,此時線程只能去申請新的Buffer,而原來的Buffer中的空閑空間就被浪費了。Buffer的大小和線程數(shù)量都會影響這些碎片的多寡。
Remember Set和Card Table
RS(Remember Set)是一種抽象概念,用于記錄從非收集部分指向收集部分的指針的集合。
在傳統(tǒng)的分代垃圾回收算法里面,RS(Remember Set)被用來記錄分代之間的指針。在G1回收器里面,RS被用來記錄從其他Region指向一個Region的指針情況。因此,一個Region就會有一個RS。這種記錄可以帶來一個極大的好處:在回收一個Region的時候不需要執(zhí)行全堆掃描,只需要檢查它的RS就可以找到外部引用,而這些引用就是initial mark的根之一。
那么,如果一個線程修改了Region內(nèi)部的引用,就必須要去通知RS,更改其中的記錄。為了達到這種目的,G1回收器引入了一種新的結(jié)構(gòu),CT(Card Table)——卡表。每一個Region,又被分成了固定大小的若干張卡(Card)。每一張卡,都用一個Byte來記錄是否修改過。卡表即這些byte的集合。實際上,如果把RS理解成一個概念模型,那么CT就可以說是RS的一種實現(xiàn)方式。
從第一感覺,或者出于直覺的考慮,使用一個bit來記錄一張卡是否被修改過,就已經(jīng)足夠了。而使用一個byte會造成更多的空間開銷。但是實際上,使用一個byte來記錄一張卡是否被修改過,會比使用一個bit來記錄效率更高。更多細節(jié)參閱資料3。
在RS的修改上也會遇到并發(fā)的問題。因為一個Region可能有多個線程在并發(fā)修改,因此它們也會并發(fā)修改RS。為了避免這樣一種沖突,G1垃圾回收器進一步把RS劃分成了多個哈希表。每一個線程都在各自的哈希表里面修改。最終,從邏輯上來說,RS就是這些哈希表的集合。哈希表是實現(xiàn)RS的一種通常的方式之一。它有一個極大的好處就是能夠去除重復(fù)。這意味著,RS的大小將和修改的指針數(shù)量相當。而在不去重的情況下,RS的數(shù)量和寫操作的數(shù)量相當。
整個關(guān)系如下:
圖中RS的虛線表名的是,RS并不是一個和Card Table獨立的,不同的數(shù)據(jù)結(jié)構(gòu),而是指RS是一個概念模型。實際上,Card Table是RS的一種實現(xiàn)方式。
Remember Set的寫屏障
寫屏障是指,在改變特定內(nèi)存的值(實際上也就是寫入內(nèi)存)的時候額外執(zhí)行的一些動作。在大多數(shù)的垃圾回收算法中,都利用到了寫屏障。寫屏障通常用于在運行時探測并記錄回收相關(guān)指針(interesting pointer),在回收器只回收堆中部分區(qū)域的時候,任何來自該區(qū)域外的指針都需要被寫屏障捕獲,這些指針將會在垃圾回收的時候作為標記開始的根。JAVA使用的其余的分代的垃圾回收器,都有寫屏障。舉例來說,每一次將一個老年代對象的引用修改為指向年輕代對象,都會被寫屏障捕獲,并且記錄下來。因此在年輕代回收的時候,就可以避免掃描整個老年代來查找根。
G1垃圾回收器的寫屏障和RS是相輔相成的,也就是記錄Region內(nèi)部的指針。這種記錄發(fā)生在寫操作之后。對于一個寫屏障來說,過濾掉不必要的寫操作是十分有必要的。這種過濾既能加快賦值器的速度,也能減輕回收器的負擔(dān)。G1垃圾回收器采用的雙重過濾
- 過濾掉同一個Region內(nèi)部引用;
- 過濾掉空引用;
過濾掉這兩個部分之后,可以使RS的大小大大減小。
G1的垃圾回收器的寫屏障使用一種兩級的log buffer結(jié)構(gòu):
- global set of filled buffer:所有線程共享的一個全局的,存放填滿了的log buffer的集合;
- thread log buffer:每個線程自己的log buffer。所有的線程都會把寫屏障的記錄先放進去自己的log buffer中,裝滿了之后,就會把log buffer放到 global set of filled buffer中,而后再申請一個log buffer;
可以內(nèi)容可以參閱資料4的11.8節(jié)
Collect Set
Collect Set(CSet)是指,在Evacuation階段,由G1垃圾回收器選擇的待回收的Region集合。G1垃圾回收器的軟實時的特性就是通過CSet的選擇來實現(xiàn)的。對應(yīng)于算法的兩種模式fully-young generational mode和partially-young mode,CSet的選擇可以分成兩種:
- 在fully-young generational mode下:顧名思義,該模式下CSet將只包含young的Region。G1將調(diào)整young的Region的數(shù)量來匹配軟實時的目標;
- 在partially-young mode下:該模式會選擇所有的young region,并且選擇一部分的old region。old region的選擇將依據(jù)在Marking cycle phase中對存活對象的計數(shù)。G1選擇存活對象最少的Region進行回收。
SATB(snapshot-at-the-beginning)
SATB(snapshot-at-the-beginning),是最開始用于實時垃圾回收器的一種技術(shù)。G1垃圾回收器使用該技術(shù)在標記階段記錄一個存活對象的快照("logically takes a snapshot of the set of live objects in the heap at the start of marking cycle")。然而在并發(fā)標記階段,應(yīng)用可能修改了原本的引用,比如刪除了一個原本的引用。這就會導(dǎo)致并發(fā)標記結(jié)束之后的存活對象的快照和SATB不一致。G1是通過在并發(fā)標記階段引入一個寫屏障來解決這個問題的:每當存在引用更新的情況,G1會將修改之前的值寫入一個log buffer(這個記錄會過濾掉原本是空引用的情況),在最終標記(final marking phase)階段掃描SATB,修正SATB的誤差。
SATB的log buffer如RS的寫屏障使用的log buffer一樣,都是兩級結(jié)構(gòu),作用機制也是一樣的。
細節(jié)可以參閱資料2,6
Marking bitmaps和TAMS
Marking bitmap是一種數(shù)據(jù)結(jié)構(gòu),其中的每一個bit代表的是一個可用于分配給對象的起始地址。舉例來說:
其中addrN代表的是一個對象的起始地址。綠色的塊代表的是在該起始地址處的對象是存活對象,而其余白色的塊則代表了垃圾對象。
G1使用了兩個bitmap,一個叫做previous bitmap,另外一個叫做next bitmap。previous bitmap記錄的是上一次的標記階段完成之后的構(gòu)造的bitmap;next bitmap則是當前正在標記階段正在構(gòu)造的bitmap。在當前標記階段結(jié)束之后,當前標記的next bitmap就變成了下一次標記階段的previous bitmap。
TAMS(top at mark start)變量,是一對用于區(qū)分在標記階段新分配對象的變量,分別被稱為previous TAMS和next TAMS。在previous TAMS和next TAMS之間的對象則是本次標記階段時候新分配的對象。如圖:
白色region代表的是空閑空間,綠色region代表是存活對象,橙色region代表的在此次標記階段新分配的對象。注意的是,在橙色區(qū)域的對象,并不能確保它們都事實上是存活的。
算法詳解
整個算法可以分成兩大部分:
- Marking cycle phase:標記階段,該階段是不斷循環(huán)進行的;
- Evacuation phase:該階段是負責(zé)把一部分region的活對象拷貝到空Region里面去,然后回收原本的Region空間,該階段是STW(stop-the-world)的;
而算法也可以分成兩種模式:
- fully-young generational mode:有時候也會被稱為young GC,該模式只會回收young region,算法是通過調(diào)整young region的數(shù)量來達到軟實時目標的;
- partially-young mode:也被稱為Mixed GC,該階段會回收young region和old region,算法通過調(diào)整old region的數(shù)量來達到軟實時目標;
有趣的地方是不論處在何種模式之下,yong region都在被回收的范圍內(nèi)。而old region只能期望于Mixed GC。但是,如同在CMS垃圾回收器中遇到的困境一樣,Mixed GC可能來不及回收old region。也就說,在需要分配老年代的對象的時候,并沒有足夠的空間。這個時候就只能觸發(fā)一次full GC。
算法會自動在young GC和mixed GC之間切換,并且定期觸發(fā)Marking cycle phase。HotSpot的G1實現(xiàn)允許指定一個參數(shù)InitiatingHeapOccupancyPercent,在達到該參數(shù)的情況下,就會執(zhí)行marking cycle phase。
算法并不使用在對象頭增加字段來標記該對象,而是采用bitmap的方式來記錄一個對象被標記的情況。這種記錄方法的好處就是在使用這些標記信息的時候,僅僅需要掃描bitmap而已。G1統(tǒng)計一個region的存活的對象,就是依賴于bitmap的標記。
Marking Cycle Phase
算法的Marking cycle phase大概可以分成五個階段:
- Initial marking phase:G1收集器掃描所有的根。該過程是和young GC的暫停過程一起的;
- Root region scanning phase:掃描Survivor Regions中指向老年代的被initial mark phase標記的引用及引用的對象,這一個過程是并發(fā)進行的。但是該過程要在下一個young GC開始之前結(jié)束;
- Concurrent marking phase:并發(fā)標記階段,標記整個堆的存活對象。該過程可以被young GC所打斷。并發(fā)階段產(chǎn)生的新的引用(或者引用的更新)會被SATB的write barrier記錄下來;
- Remark phase:也叫final marking phase。該階段只需要掃描SATB(Snapshot At The Beginning)的buffer,處理在并發(fā)階段產(chǎn)生的新的存活對象的引用。作為對比,CMS的remark需要掃描整個mod union table的標記為dirty的entry以及全部根;
- Cleanup phase:清理階段。該階段會計算每一個region里面存活的對象,并把完全沒有存活對象的Region直接放到空閑列表中。在該階段還會重置Remember Set。該階段在計算Region中存活對象的時候,是STW(Stop-the-world)的,而在重置Remember Set的時候,卻是可以并行的;
Initial marking phase
該階段掃描所有的根,與CMS類似。所不同的是,該階段是和young GC一起的。這里的young GC實際上是指的就是fully-young generational mode。
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Guide的原文是"This phase is piggybacked on a normal (STW) young garbage collection"。
Root region scanning phase
該過程主要是掃描Survivor region中指向老年代的,在initial mark phase標記的引用及其引用的對象。這是一個很奇怪的步驟,因為在前面不論是Parallel Collector還是CMS,都沒有這么一個步驟。
要理解這一點,要注意的是,算法的兩種模式,不論是young GC還是mixed GC,都需要回收young region。因為實際上RS是不記錄從young region出發(fā)的指針,例如,這部分指針包括young region - young region,也包括young-region - old region指針。那么就可能出現(xiàn)一種情況,一個老年代的存活對象,只被年輕代的對象引用。在一次young GC中,這些存活的年輕代的對象會被復(fù)制到Survivor Region,因此需要掃描這些Survivor region來查找這些指向老年代的對象的引用,作為并發(fā)標記階段掃描老年代的根的一部分。
在理解了這一點的基礎(chǔ)上,那么對于階段必須在下一次young GC啟動前完成的要求,也就理解了。因為如果第二次的young GC啟動了,那么這個過程中,survivor region就可能發(fā)生變化。這個時候執(zhí)行root region phase就會產(chǎn)生錯誤的結(jié)果。
Concurrent marking phase
在標記階段,會使用到一個marking stack的東西。G1不斷從marking stack中取出引用,遞歸掃描整個堆里的對象圖,并且在bitmap上進行標記。這個遞歸過程采用的是深度遍歷,會不斷把對象的域入棧。
在并發(fā)標記階段,因為應(yīng)用還在運行,所以可能會有引用變更,包括現(xiàn)有引用指向別的對象,或者刪除了一個引用,或者創(chuàng)建了一個新的對象等。G1采用的是使用SATB的并發(fā)標記算法。
在資料6中記錄了使用SATB的兩條原則:
- All accessible cells at the beginning of the garbage collection are eventually marked during the marked phase;
- Newly alocated cells during the garbage collection are never collected during the sweep phase of that garbage collection
在G1中,該算法的關(guān)鍵在于,如果在并發(fā)標記的時候,出現(xiàn)了引用修改(不包含新分配內(nèi)存給對象),那么寫屏障會把這些引用的原始值捕獲下來,記錄在log buffer中。而后再處理。后續(xù)的所有的標記,都是從原來的值出發(fā),而不是從新的值出發(fā)的。
SATB是一個邏輯上存在概念,在實際中并沒有任何真的實際的數(shù)據(jù)結(jié)構(gòu)與之對應(yīng)。叫這個名字,是因為,一旦進入了concurrent marking階段,那么該在該階段的運行過程中,即便應(yīng)用修改了引用,但是因為SATB的寫屏障記錄下來了原始的值,在遍歷整個堆查找存活對象的時候,使用的依然是原來的值。這就是在邏輯上保持了一個snapshot at the beginning of concurrent marking phase。
在處理新創(chuàng)建的對象,G1采用了不同的方式。G1用了兩個TAMS變量了判斷新創(chuàng)建的對象。一個叫做previous TAMS,一個叫做next TAMS。位于兩者之間的對象就是新分配的對象。
并發(fā)標記階段,bitmap和TAMS的作用如圖:
注:圖片引自資料2
該圖的詳細解釋如下:
- A是第一次marking cycle的initial marking階段。next bitmap尚未標記任何存活對象,而此時的previous TAMS被初始化為region內(nèi)存地址起始值,next TAMS被初始化為top。top實際上就是一個region未分配區(qū)域和已分配區(qū)域的分界點;
- B是經(jīng)過concurrent marking階段之后,進入了remark階段。此時存活對象的掃描已經(jīng)完成了,因此next bitmap構(gòu)造好了,剛好代表的是當下狀態(tài)中region中的內(nèi)存使用情況。注意的是,此時top已經(jīng)不再與next TAMS重合了,top和next TAMS之間的就是在前面標記階段之時,新分配的對象;
- C代表的是clean up階段。C和B比起來,next bitmap變成了previous bitmap,而在bitmap中標記為垃圾(也就是白色區(qū)域的)的對應(yīng)的region的區(qū)域也被染成了淺灰色。這并不是指垃圾對象已經(jīng)被清掃了,僅僅是標記出來了。同時next TAMS和previous TAMS也交換了角色;
- D代表的是下一個marking cycle的initial marking階段,該階段和A類似,next TAMS重新被初始化為top的值;
- EF就是BC的重復(fù);
Remark phase
該階段是一個STW的階段。引入該階段的目的,是為了能夠達到結(jié)束標記的目標。要結(jié)束標記的過程,要滿足三個條件:
- concurrent marking已經(jīng)追蹤了所有的存活對象;
- marking stack是空的;
- 所有的log都被處理了;
前兩個條件是很容易達到的,但是最后一個是很困難的。如果不引入一個STW的remark過程,那么應(yīng)用會不斷的更新引用,也就是說,會不斷的產(chǎn)生log,因而永遠也無法達成完成標記的條件。
Clean up
該階段主要完成:
- 統(tǒng)計存活對象,這是利用RS和bitmap來完成的,統(tǒng)計的結(jié)果將會用來排序region,以用于下一次的CSet的選擇;
- 重置RSet;
- 把空閑region放到空閑region列表中;
該階段比較容易引起誤解地方在于,Clean up并不會清理垃圾對象,也不會執(zhí)行存活對象的拷貝。也就是說,在極端情況下,該階段結(jié)束之后,空閑Region列表將毫無變化,JVM的內(nèi)存使用情況也毫無變化。
Evacuation
Evacuation階段STW的,大概可以分成兩個步驟:第一個步驟是從Region中選出若干個Region進行回收,這些被選中的Region稱為Collect Set(簡稱CSet);而第二個步驟則是把這些Region中存活的對象復(fù)制到空閑的Region中去,同時把這些已經(jīng)被回收的Region放到空閑Region列表中。
這兩個步驟又可以被分解成三個任務(wù):
- 根據(jù)RS的日志更新RS:只有在處理完了RS的日志之后,RS才能夠保證是準確的,完整的,這也是Evacuation是STW的重要原因;
- 掃描RS和其余的根來確定存活對象:該階段實際上最主要依賴于RS;
- 拷貝存活對象:該階段只要從2中確定的根觸發(fā),沿著引用鏈一直追溯下去,將存活對象復(fù)制到新的region就可以。這個過程中,可能有一部分的年輕代對象會被提升到老年代;
Evacuation的時機
Evacuation的觸發(fā)時機在不同的模式下會有一些不同。在不同的模式下都相同的是,只要堆的使用率達到了某個閾值,就必然會觸發(fā)Evacuation。這是為了確保在Evacuation的時候有足夠的空閑Region來容納存活對象。
在young GC的情況下,G1會選擇N個region作為CSet,該CSet首先需要滿足軟實時的要求,而一旦已經(jīng)有N個region已經(jīng)被分配了,那么就會執(zhí)行一次Evacuation。
G1會盡可能的執(zhí)行mixed GC。唯一的限制就是mix GC也需要滿足軟實時的要求。
G1觸發(fā)Evacuation的原則大概是:
- 如果被分配的young region數(shù)量滿足young GC的要求,那么就會觸發(fā)young GC;
- 如果被分配的young region數(shù)量不滿足young GC,就會進一步考察加上old region的數(shù)量,能否滿足old GC的要求;
為了理解這一點,可以舉例來說,假如回收一個old region的時間是回收一個young region的兩倍,也就是young region花費時間T,old region花費2T,在滿足軟實時目標的情況下,GC只能回收8T的region,那么:
- 假如應(yīng)用現(xiàn)在只分配k(k<8)塊young region,沒有分配任何old region。這個時候又分配了一個old region,那么這個時候會立刻觸發(fā)一次mixed GC,此次GC會選擇k塊young region和一塊old region;
- 因此,在這種假設(shè)下,只要有可以回收的old region的時候,總是會先回收old region;
- 在沒有任何old region的情況下,才有可能觸發(fā)young region。
當然,在一般情況下,這些假設(shè)是不成立的。讀者可以思考一下,在young GC和mixed GC達到軟實時的要求下,young region和old region之間回收的花銷不同會導(dǎo)致young GC和mixed GC會在什么情況下觸發(fā)。
資料
- Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide
- David Detlefs, Christine Flood, Steve Heller, Tony Printezis. Garbage-First Garbage Collection
- Urs Ho?lzle. A Fast Write Barrier for Generational Collectors
- 垃圾回收算法手冊——自動內(nèi)存管理的藝術(shù)
- 請教G1算法的原理——RednaxelaFX的回答
- Taichi Yuasa. Real-time garbage collection on general-purpose machines.
- Christine H. Flood, David Detlefs, Nir Shavit, and Xiaolan Zhang. Parallel garbage collection for shared memory multiprocessors
- 名詞連接貼-RednaxelaFX對Remeber Set和Card Table的解釋
- G1: One Garbage Collector To Rule Them All
- Poonam Parhar. Understanding G1 GC Logs
- Getting Started with the G1 Garbage Collector