JVM-G1算法和數據結構那些事

本文來自OPPO互聯網技術團隊,轉載請註名做者。同時歡迎關注咱們的公衆號:OPPO_tech,與你分享OPPO前沿互聯網技術及活動。html

人的狀況和樹相同。它愈想開向高處和明亮處,它的根愈要向下,向泥土,向黑暗處,向深處,向惡——千萬不要忘記。咱們飛翔得越高,咱們在那些不能飛翔的人眼中的形象越是眇小。

——尼采《查拉圖斯特拉如是說》java

每每,最基礎最底層的知識裏,蘊含着原始而強大的力量。本文將以java8 SE HotSpot VM爲依據,盤點G1裏的算法和數據結構。算法

本文將講述:數組

  • 三色標記法:解釋了爲何併發類回收器須要從新標記和stw的過程;
  • card table&remember set:解釋了G1爲何能夠在GC時不用掃描整個年老代從而對大堆更友好;
  • satb:解釋了G1如何處理併發標記過程當中的新增對象和引用變動以及浮動垃圾從何而來;
  • collection sets:簡要說明爲何G1能夠實現回收時間大體可控。

三色標記法

併發類回收器的併發標記階段,gc線程和應用線程是併發執行的。因此一個對象被標記以後,應用線程可能篡改對象的引用關係,從而形成對象的漏標、誤標。數據結構

其實誤標沒什麼關係,頂多形成浮動垃圾,在下次gc仍是能夠回收的。可是漏標的後果是致命的,把本應該存活的對象給回收了,從而影響的程序的正確性。併發

爲了解決在併發標記過程當中,存活對象漏標的狀況,GC HandBook把對象分紅三種顏色:oracle

  • 黑色:根對象,或者該對象與它的子對象都被掃描
  • 灰色:對象自己被掃描,但還沒掃描完該對象中的子對象
  • 白色:未被掃描對象,掃描完成全部對象以後,最終爲白色的爲不可達對象,即垃圾對象。

當GC開始掃描對象時,按照以下圖步驟進行對象的掃描:ide

根對象被置爲黑色,子對象被置爲灰色;oop

繼續由灰色遍歷,將已掃描了子對象的對象置爲黑色;post

遍歷了全部可達的對象後,全部可達的對象都變成了黑色;不可達的對象即爲白色,須要被清理。

這看起來彷佛很美好,然而--若是在標記過程當中,應用程序也在運行,那麼對象的指針就有可能改變。這樣的話,咱們就會遇到一個問題:對象丟失問題

咱們看下面一種狀況,當垃圾收集器掃描到下面狀況時:

這時候應用程序執行了如下操做:

A.c=C

B.c=null

這樣,對象的狀態圖變成以下情形:

很顯然,此時C是白色,被認爲是垃圾須要清理掉,顯然這是不合理的。

一個已經被灰對象指向白對象,在併發標記階段會被漏標的充分必要條件是:

  • Mutator 插入了一個 黑對象 到 該白對象的引用;
  • Mutator 刪除了全部 灰對象 到 該白對象的引用。

上面兩個充要條件,只要打破一個就不會被漏標。就有以下兩種可行的方式解決漏標:

  • 在插入的時候記錄對象;
  • 在刪除的時候記錄對象。

恰好這對應CMS和G1的2種不一樣實現方式——

1、CMS採用的是增量更新(Incremental update)(post-write barrier),致力於第一個條件的打破,只要在寫屏障(write barrier)裏發現要有一個白對象的引用被賦值到一個黑對象 的字段裏,那就把這個白對象變成灰色的,即插入的時候記錄下來。哪怕刪除全部灰對象到該白對象的引用,remark 階段從新回顧一次就不會漏標了

2、G1中,使用的是STAB(snapshot-at-the-beginning)(pre-write barrier)的方式,致力於第二個條件的打破。採用最保守的作法,把變動前的引用對象記錄下來,看成是存活對象,讓其活過這一週期。

[NextTAMS,top]指針之間的對象是併發標記期間新增對象,也在這一個週期裏隱式存活。

所以 G1 的 SATB 會產生更多的浮動垃圾。可是換來的好處就是:不須要像 CMS 那樣 remark,再走一遍 root trace 這種至關耗時的流程。

它有三個步驟:

這樣,G1到如今能夠知道哪些老的分區可回收垃圾最多。當全局併發標記完成後,就開始Mix GC。

Card Table&Remembered Set

年老代在堆中的比例遠大於年輕代,年輕代比較小即便全堆掃描也成本不高,但若是每次YGC或Mixed GC都要掃描一次年老代全堆的話,確定會很是耗時,那麼有什麼好的解決方案呢?

JVM G1回收器使用了card table和remember set兩個結構處理年老代全堆掃描的問題。

Card Table

基於卡表(Card Table)的設計,一般將堆空間劃分爲一系列2次冪大小的卡頁(Card Page),HotSpot JVM的卡頁大小爲512字節。

卡表維護着全部的Card Page。Card Table的結構是一個字節數組,每一個卡表項映射着一個Card Page,爲1個字節,用於標記卡頁的狀態。

Card Page中對象的引用發生改變時,寫屏障邏輯將會把Card Page在Card Table數組中對應的值標記爲dirty,就稱這個Card Page被髒化了。

因此Card Table其實就是映射着內存中的對象,在進行Young GC的時候,即可以不用掃描整個老年代。

而是在卡表中尋找髒卡,並將髒卡中的對象加入到Minor GC的GC Roots裏。當完成全部髒卡的掃描以後,全部髒卡的標識位清零。

對於一些熱點Card Page會存放到Hot Card Cache中。同Card Table同樣,Hot Card Cache也是全局的結構。

OpenJDK/Oracle 1.6/1.7/1.8 JVM默認的卡標記簡化邏輯以下:

CARD_TABLE [this address >> 9] = 0;

首先:計算對象引用所在卡頁的卡表索引號。將卡頁地址右移9位,至關於用地址除以512(2的9次方)。能夠這麼理解:假設卡表卡頁的起始地址爲0,那麼卡表項0、一、2對應的卡頁起始地址分別爲0、5十二、1024(卡表項索引號乘以卡頁512字節)。

其次:經過卡表索引號,設置對應卡標識爲dirty。

Card table隨着堆內存一塊兒初始化,全局惟一,經過具體的垃圾收集策略進行建立。

Remembered Set

每個Region都有本身的RSet。

虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中。若是是,便經過Card Table把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。

維繫RSet中的引用關係靠post-write barrier&Concurrent refinement threads來維護,操做僞代碼以下:

void oop_field_store(oop* field, oop new_value) {
// pre-write barrier: for maintaining SATB invariant
// the actual store
  pre_write_barrier(field);  
*field = new_value;   
// post-write barrier: for tracking cross-region reference
  post_write_barrier(field, new_value);
}

RSet裏面記錄了引用--就是其餘Region中指向本Region中全部對象的全部引用,也就是誰引用了個人對象。

RSet實際上是一個Hash Table,Key是其餘的Region的起始地址,Value是一個集合,裏面的元素是Card Table 數組中的index,既Card對應的Index,映射到對象的Card地址。

好比A對象在regionA,B對象在regionB,且B.f = A,則在regionA的RSet中須要記錄一對鍵值對,key是regionB的起始地址,Value的值能映射到B所在的Card的地址,因此要查找B對象,就能夠經過RSet中記錄的卡片來查找該對象。

本分區對象引用本分區本身的對象,這種引用不用落入RSet中;同時,G1 GC每次都會對年輕代進行總體收集。

所以young->old和young->young也不須要在RSet中記錄。而對於old->young和old->old的跨代對象引用,須要擁有RSet。

對於G1進行YGC時的跨代引用,以及進行Mixed GC時的old 間跨region引用,只要到本Region RSet所記錄的region中掃描引用了髒card區域的對象,再如法溯源,判斷其是否存活存活,進而肯定本分區內的對象存活狀況。而不須要掃描整個堆了。

爲了防止RSet溢出,對於一些比較熱點的RSet會經過存儲粒度級別來控制。

RSet有三種粒度—Sparse (稀疏) &Fine (細) &Coarse (粗)

對於熱點RSet在存儲時,根據細粒度的存儲閥值,可能會採起粗粒度。這三種粒度的RSet都是經過Per Region Table來維護內部數據的。一個Per-Region-Table (PRT)是RSet存儲顆粒度級別一個抽象。

Sparse PRT是一個包含Card目錄的Hash Table:G1收集器內部維護這些Card。Card包含來自Region的引用,這個Region的引用是Card到Owning Region的關聯的地址。

Fine-Grain PRT是一個開放的Hash Table:每個Entry表明一個指向Owning Region的引用的Region,Region裏面的Card目錄,是一個Bitmap。

當達到Fine-Grain PRT的最大容量,Coarse Grain Bitmap裏面的相應的Coarse-Grained bit被設置,相應地Entry從Fine-Grain PRT刪除。

Coarse bitmap有一個每一個Region對應的bit。Coarse Grain map設置bit意味着關聯的Region包含到Owning Region的引用。

SATB

SATB(Snapshot-At-The-Begin)之因此叫這個名字,就是在初始標記開始時,G1 收集器打了一個快照,造成一個所謂的對象圖(Object Graph)。

這個對象圖記錄在 next marking bitmap 之中 ,在併發標記階段會在這個 bitmap 中 記錄對象存活標記。最終Remark階段結束後,完成對快照對象圖全部標記。

圖中能夠很明確看到兩個bitmap數據結構——G1 是藉助 bitmap 來存放對象存活標記。每個 bit 表示每一個region中的某個對象起始地址,若是 bit 標記爲 1(黑色),則表示該對象存活,bit 與對象對應有一套算法:

  • Bottom 指向 Region起點位置;
  • Top 永遠指向當前Region 最新分配的對象,記錄其起始位置;
  • PrevTAMS 和 NextTAMS 分別標記先後兩次併發標記週期開始時,Top 指針的位置(TAMS - top at mark start);
  • End 表示 Region 終點位置。

[Bottom,PrevTAMS)-> 這部分的存活信息會在previous marking bitmap體現;

[Bottom, NextTAMS)-> 當清理時,PrevTAMS指向NextTAMS地址,NextTAMS歸零,全部垃圾對象能經過[ Bottom, previousTAMS ]之間的對象快照被識別出來;

[NextTAMS, Top)-> 這部分對象在第 n 輪全局標記週期是隱式存活,SATB可以確保這部分的對象都會被標記,保障併發標記期間新增的對象不會被清理;

SATB利用pre-write barrier,將全部即將被修改引用關係的白對象舊引用記錄下來,最後以這些舊引用爲根從新掃描一遍,以解決白對象引用被修改產生的漏標問題。在引用關係被修改以前,插入一層pre-write barrier,代碼以下:

pre-write barrier最終執行邏輯:

//openjdk/hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp lines 52 ~ 65
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
    // Nulls should have been already filtered.
    assert(pre_val->is_oop(true), "Error");
    if (!JavaThread::satb_mark_queue_set().is_active()) return;
    Thread* thr = Thread::current();
    if (thr->is_Java_thread()) {
        JavaThread* jt = (JavaThread*)thr;
        jt->satb_mark_queue().enqueue(pre_val);
    } else {
        MutexLocker x(Shared_SATB_Q_lock);
       JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
}
}

經過

G1SATBCardTableModRefBS::enqueue(oop pre_val)

把原引用保存到satb_mark_queue中,和RSet的實現相似,每一個應用線程都自帶一個satb_mark_queue。在下一次的併發標記階段,會依次處理satb_mark_queue中的對象,確保這部分對象在本輪GC是存活的。

然而,SATB也是有反作用的。若是被修改引用的白對象就是要被收集的垃圾,此次的標記會讓它躲過GC,這就是float garbage。由於SATB的作法精度比較低,因此形成的float garbage也會比較多。

Collection Sets

Collect Set (CSet)是指:在Evacuation階段,由G1垃圾回收器選擇的待回收的Region集合。G1垃圾回收器的軟實時的特性就是經過CSet的選擇來實現的。

對於年輕代收集:CSet只容納Eden Regions、Survivor Region;

對於混合收集:CSet還會容納1/8的老年代Region。

G1將調整young的Region的數量來匹配軟實時的目標;old region的選擇將依據在Marking cycle phase中對存活對象的計數,G1選擇存活對象最少的Region進行回收。

回收後CSet全部分區都會被釋放,內部存活的對象都會被轉移到分配的空閒Region中。

最後

成文時間所限,不免校對勘誤疏漏,諸位看官還請寬容本人的愚鈍,歡迎補充指正。

參考文獻:

  1. https://docs.oracle.com/javas...
  2. https://docs.oracle.com/javas...
  3. https://www.jishuwen.com/d/2M...
  4. https://docs.cloudera.com/HDP...
  5. https://storage/content/recom...

相關文章
相關標籤/搜索