深刻學習 G1回收器和JVM:Refine線程(4)

記憶集(RSet)

RSet使用 根對象引用的收集算法
  • ObjA.Filed = ObjB算法

    • point out : 在ObjA中的RSet記錄ObjB的位置
    • point in : 在ObjB中的RSet記錄ObjA的位置
    • RSet使用的是point in

G1提供的3種回收算法:

  1. 新生代回收:老是收集全部新生代分區
  2. 混合回收:收集全部新生代分區和部分老生代分區
  3. Full GC:處理全部分區

分區之間的引用關係

這裏指分區裏有一個對象存在一個指針指向另外一個分區的對象
  1. 分區內部有引用關係
  2. 新生代分區之間有引用關係
  3. 新生代分區到老生代分區之間有引用關係
  4. 老年代分區到新生代分區之間有引用關係
  5. 老年代分區之間有引用關係

RSet回收的缺點:

  1. 須要額外的內存空間;這部分一般是JVM最大的額外開銷,通常在1%~20%。
  2. 可能致使浮動垃圾。(RSet裏的內容可能已死亡,這個時候)

哪些須要記錄在RSet中的引用關係

  1. 須要:數組

    1. 老年代分區到新生代分區之間的引用緩存

      • YoungGC的時候有兩種根數據結構

        • 棧空間/全局空間變量的引用
        • 老生代分區到新生代分區之間的引用
    2. 老年代分區到老年代分區之間的引用併發

      混合GC的時候可能只有部分分區被回收,必須記錄引用關係,快速找到哪些對象是活躍的。
  2. 不須要:異步

    1. 分區內部的引用關係。性能

      對於一個分區來講,只有回收和不回收,回收的時候就會遍歷整個分區,因此無需記錄這種引用關係。
    2. 新生代分區之間的引用關係:線程

      G1的3種回收算法都會處理全部的新生代分區,回收的時候會遍歷全部的新生代分區。
    3. 新生代分區到老生代分區之間的引用設計

      對於YoungGC來講,針對的新生代,則無需關心;對於混合GC來講,會使用新生代分區做爲根,那麼遍歷全部新生代分區天然能找到老年代;對於FullGC來講,全部分區都會被清理,無需關心引用關係。

RSet和卡表的關係

RSet記錄引用者的地址
  • 咱們若是直接記錄對象地址,帶來的問題就是RSet會急劇膨脹(一個對象被引用的次數不固定,可能不少也可能不多)。一個位能夠表示512個字節區到被引用區的關係。RSet用分區的起始地址和位圖表示一個分區全部的引用信息。
  • 在G1中,算法能夠簡化爲找到須要收集的分區HR集合,因此YoungGC掃描Root Set和RSet就能夠了。
  • 卡表是個全局表,做用並非記錄引用關係,而是記錄該區域中對象垃圾回收過程當中的狀態信息,且能描述對象所處的內存區域塊。

新數據結構 - PRT(Per region Table):分區記錄全部引用者的信息

每一個HR裏都包含了一個PRT,它是經過HR中的一個結構 HeapRegionRemSet得到,而每一個HeapRegionRemSet包含了一個OtherRegionsTable,也就PRT。
OtherRegionTable使用了3種粒度來描述引用
  • 稀疏PRT:經過哈希表來存儲。默認長度是4。
  • 細粒度PRT:經過PRT指針的數組,數組長度能夠指定,也能夠自動根據計算獲得。
  • 粗粒度:經過位圖來指示,每一位表示對應的分區有引用到該分區數據結構。

Refine線程的功能和原理

Refine線程是G1新引入的併發線程池,線程默認數爲 G1ConcTefinementThreads+1

Refine的兩大功能:

  • 用於處理新生代分區的抽樣,並在知足響應時間的指標下,更新YHR的數目。
  • 管理RSet。這個是Refine最主要的功能。指針

    • RSet的更新不是同步完成的,G1會把全部的引用關係放入一個隊列中,稱爲Dirty Card Queue(DCQ),而後使用線程來消費這個隊列以完成更新。
    • 實際上除了Refine線程更新RSet以外,GC線程或者Mutator也可能會更新RSet。
    • DCQ經過Dirty Card Queue Set(DCQS)來管理。
    • 爲了可以併發處理,每一個Refine線程只負責DCQS中的某幾個DCQ

抽樣線程

Refine線程池中的最後一個線程就是抽樣線程,主要做用是用來設置新生代分區的個數,使G1知足垃圾回收的停頓預測時間。

管理RSet

G1使用Refine線程 異步維護和管理引用關係。
  • JVM聲明瞭一個全局的靜態變量 DirtyCardQueueSet(DCQS)
  • DCQS裏面存放的是DCQ
  • 爲了性能考慮,全部處理引用關係的線程共享一個DCQS。
  • 每一個Mutator線程在初始化的時候都關聯這個DCQS。
  • 每一個Mutator線程都有一個私有的隊列,隊列的最大長度由G1UpdateBufferSize(默認256)決定,即默認最大存放256個引用關係對象。
  • 在Mutator線程中,若是產生新的對象的引用關係,則把引用者放入DCQ中,當滿256個時,就會把這個DCQ放入DCQS中(放入的時候須要加鎖)。
  • 若是沒有滿256個時,也能夠手動提交(須要指明有多個引用關係)。
  • 當Refine線程忙不過來的時候,G1讓Mutator幫忙處理引用變動。
  • Refine線程的個數能夠有用戶設置。

Mutator處理DCQ

  • DCQS的最大長度依賴與Refine線程的個數,最大爲RedZone的個數。
  • DCQS裏面的DCQ個數超過RedZone的個數時,Mutator就不能把這個DCQ放入set中,這時候Mutator就會直接處理這個隊列的引用。

Refine線程的工做原理

  • Refine線程的初始化是在GC管理器初始化的時候進行。JVM經過wait和notify機制實現。

    • 從 0 到 n-1 線程(n表示線程個數),當前一個線程發現本身太忙,則啓動後面一個。
    • 當線程發現本身太閒,則主動凍結本身。
    • 第0個線程何時被激活?

      • 當mutator線程嘗試把DCQ放入DCQS時,若是發現0號線程沒有被激活,則發送notify激活。
    • 因此第0個線程是由任意mutator線程激活,1 到 n-1 線程只能由前一個線程激活。因此0號線程等待的monitor是個全局變量,而 1 到 n-1線程中的monitor是局部變量。
    • RSet的更新流程簡單總結就是:根據引用者找到被引用者,而後在被引用者的RSet中記錄引用關係。
    • Refine線程執行的過程不會發生GC,因此不會產生對象的移動。
    • 有可能過多的RSet更新會致使mutator很慢(mutator會主動幫忙Refine線程處理)

Refinement Zone

咱們能夠設置多個Refine線程工做,在不一樣的負載下啓用的線程不一樣。這個工做負載就經過Refinement Zone控制。
G1提供3個值, Green, Yellow, Red,將整個Queue Set分爲4個區。姑且稱爲白,綠,黃,紅
    • [0,Green),對於該區,Refine線程不處理,交給GC線程來處理DCQ。
    • [Green,Yellow),在該區中,Refine線程開始啓動,根據Queue Set數值的大小啓動不一樣的Refine線程來處理DCQ。
    • 使用參數G1ConcRefinementThresholdStep來控制每一個Refine線程消費隊列的步長,若是不設置,則自動推斷爲Refine線程+1
    • [Yellow,Red),在該區中,全部的Refine線程(除了抽樣線程)都參與DCQ處理。
    • [Red, + ∞),在該區中,不只全部的Refine線程參與處理RSet,並且連Mutator線程也參與處理。
這3個值經過三個參數處理,默認值都爲0,若是不設置,則G1自動推斷三個值大小。
  • G1ConcRefinementGreenZoneParallelGCThreads
  • G1ConcRefinementYellowZoneG1ConcRefinementGreenZone 的3倍
  • G1ConcRefinementRedZoneG1ConcRefinementGreenZone 的6倍

全部Refine線程是有幾個線程?

  • 能夠經過G1ConcRefinementThreads設置,默認爲0
  • 沒有設置的時候G1啓發式推斷,設置爲ParallelGCThreads.
  • ParallelGCThreads也根據參數設置,默認爲0。
  • ParallelGCThreads沒有設置時,G1也經過啓發式推斷
  • ParallelGCThreads = ncpus(cpu內核個數)

    • 當ncpus <= 8,ncpus爲 8 +(ncpus - 8)*5/8
    • 當ncpus > 8,ncpus爲cpu內核個數
  • 假設 ParallelGCThreads = 4 ,G1ConcRefinementThreads =3

    • G1ConcRefinementThresholdStep = 黃區個數 - 綠區個數/(worknum + 1),自動推斷爲2
    • 則綠黃紅個數爲 4,12,24
    • 這裏有4個Refine線程

      • 0號線程:DCQ超過4個的時候啓動,低於4個終止
      • 1號線程:DCQ超過到達9個啓動,低於6個終止
      • 2號線程:DCQ達到11個啓動,低於8個終止
      • 3號線程:處理新生代的抽樣
      • 當DCQ超過24個,Mutator開始幫忙處理DCQ

RSet涉及的寫屏障

寫屏障是指在改變特定內存的值時(實際上就是寫入內存),額外執行的一些動做。
寫屏障一般用於在運行時探測並記錄回收相關指針,在回收器只回收堆中部分區域的時候,任何來自該區域外的指針都會被寫屏障捕獲,這些指針將會在垃圾回收的時候做爲標記開始的根。
CMS中也是經過寫屏障記錄引用關係。
每一次將一個老年代對象的引用修改成指向新生代對象,都會被寫屏障捕獲並記錄下來。所以在新生代回收的時候,就能夠避免掃描整個老年代來查找根。

G1寫屏障採用三重過濾沒必要要的寫操做:

  1. 不記錄新生代到新生代的引用或者新生代到老年代的引用。

    • 由於在垃圾回收時,新生代的堆分區都會被回收
  2. 過濾同一個分區內部引用,在RSet處理時過濾。
  3. 過濾掉空引用,在RSet處理時過濾。
過濾後就能使RSet的佔用空間大大減小。
垃圾回收的寫屏障使用一種兩集的緩存結構(用queue set 實現)
  • 線程queue set : 每一個線程有本身的queue set。全部線程都會把寫屏障的記錄先放入本身的queue set中,裝滿以後,就會把queue set 放入global set of filled queue中。然後再申請一個新的queue set。
  • global set of filled buffer:全部線程共享的一個全局的,存放填滿了的DCQS集合。

參數介紹和調優

  • 參數G1ConcRefinementThreads,指的是G1Refine線程的個數,默認值爲0,G1能夠啓發式推斷,將並行的線程數ParallelGCThreads做爲併發線程數,其中並行線程數能夠設置,也能夠啓發式推斷。一般你們不用設置這個參數,並行線程數能夠簡單總結爲CPU個數的5/8,具體的推斷方法見上文。
  • 參數G1UpdateBufferSize,指的是DCQ的長度,默認值是256,增大該值能夠保存更多的待處理引用關係。
  • 參數G1UseAdaptiveConcRefinement,默認值爲true,表示能夠動態調整RefinementZone的數字區間,調整的依據在於RSet時間是否知足目標時間。
  • 參數G1RSetUpdatingPauseTimePercent,默認值爲10,即RSet所用的所有時間不超過GC完成時間的10%。若是超過而且設置了參數G1UseAdaptiveConcRefinement爲true,更新GreenZone的方法爲:當RSet處理時間超過目標時間,Greenzone變成原來的0.9倍,不然若是更新的處理過的隊列大於GreenZone,增大Greenzone爲原來的1.1倍,不然不變;對於YellowZone和RedZone分別爲GreenZone的3倍和6倍。這裏特別要注意的是當動態變化時,可能致使GreenZone爲0,那麼YellowZone和RedZone都爲0,若是這種狀況發生,意味着Refine線程再也不工做,利用Mutator來處理RSet,這一般絕非咱們想要的結果。因此在設置的時候,能夠關閉動態調整,或者設置合理的RSet處理時間。關閉動態調整須要有更好的經驗,因此設置合理的RSet處理時間更爲常見。
  • 參數G1ConcRefinementThresholdStep,默認值爲0,若是沒有定義G1會啓發式推斷,依賴於YellowZone和GreenZone。這個值表示的是多個更新RSet的Refine線程對於整個DirtyCardQueueSet的處理步長。
  • 參數G1ConcRefinementServiceIntervalMillis,默認值爲300,表示RS對新生代的抽樣線程間隔時間爲300ms。
  • 參數G1ConcRefinementGreenZone,指定GreenZone的大小,默認值爲0,G1能夠啓發式推斷。若是設置爲0,那麼當動態調整關閉,將致使Refine工做線程不工做,若是不進行動態調整,意味着GC會處理全部的隊列;若是該值不爲0,表示Refine線程在每次工做時會留下這些區域,不處理這些RSet。這個值若是須要設置生效的話,要把動態調整關閉。一般並不設置這個參數。
  • 參數G1ConcRefinementYellowZone,指定YellowZone的大小,默認值爲0,G1能夠啓發式推斷,是GreenZone的3倍
  • 參數G1ConcRefinementRedZone,指定RedZone的大小,默認值爲0,G1能夠啓發式推斷,是GreenZone的6倍,一般來講並不須要調整G1ConcRefinementGreenZone、G1ConcRefinementYellowZone和G1ConcRefinementRedZone這3個參數,可是若是遇到RSet處理太慢的狀況,也能夠關閉G1UseAdaptiveConcRefinement,而後根據Refine線程數目設置合理的值。
  • 參數G1ConcRSLogCacheSize,默認值爲10,即存儲hotcard最多爲210,也就是1024個。那麼超過1024個該如何處理?實際上JVM設計得很簡單,超過1024,直接把老的那個card拿出去處理,至關於認爲它再也不是hotcard。
  • 參數G1ConcRSHotCardLimit,默認值爲4,當一個card被修改4次,則認爲是hotcard,設計hotcard的目的是爲了減小該對象修改的次數,由於RSet在被引用的分區存儲,因此可能有多個對象引用這個對象,再處理這個對象的時候,能夠一次性地把這多個對象都做爲根。
  • 參數G1RSetRegionEntries,默認值爲0,G1能夠啓發式推斷。base*(log(region_size/1M)+1),base的默認值是256,base僅容許在開發版本設置,在發佈版本不能更改base。這個值很關鍵,過小將會致使RSet的粒度從細變粗,致使追蹤標記對象將花費更多的時間。另外,從上面的公式中也能夠獲得:經過調整HeapRegionSize來影響該值的推斷,如人工設置HeapRegionSize。實際工做中也能夠根據業務狀況直接設置該值(如設置爲1024);這樣能保持較高的性能,此時每一個分區中的細粒度卡表都使用1024項,全部分區中這一部分佔用的額外空間加起來就是個不小的數字了,這也是爲何RSet浪費空間的地方。
  • 參數G1SummarizeRSetStats打印RSet的統計信息,G1SummarizeRSetStatsPeriod=n,表示GC每發生n次就統計一次,默認值是0,表示不會週期性地收集信息。在生產中一般不會使用信息收集。
相關文章
相關標籤/搜索