G1並發標記過程觀點不錯的文章


原文地址 https://www.jianshu.com/p/aef0f4765098 

Marking Cycle Phase

算法的Marking cycle phase大概可以分成五個階段:

  1. Initial marking phase:G1收集器掃描所有的根。該過程是和young GC的暫停過程一起的;
  2. Root region scanning phase:掃描Survivor Regions中指向老年代的被initial mark phase標記的引用及引用的對象,這一個過程是並發進行的。但是該過程要在下一個young GC開始之前結束;
  3. Concurrent marking phase:並發標記階段,標記整個堆的存活對象。該過程可以被young GC所打斷。並發階段產生的新的引用(或者引用的更新)會被SATB的write barrier記錄下來;
  4. Remark phase:也叫final marking phase。該階段只需要掃描SATB(Snapshot At The Beginning)的buffer,處理在並發階段產生的新的存活對象的引用。作為對比,CMS的remark需要掃描整個mod union table的標記為dirty的entry以及全部根;
  5. 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出發的指針,例如,這部分指針包括young region - young region,也包括young-region - old region指針。那么就可能出現一種情況,一個老年代的存活對象,只被年輕代的對象引用。在一次young GC中,這些存活的年輕代的對象會被復制到Survivor Region,因此需要掃描這些Survivor region來查找這些指向老年代的對象的引用,作為並發標記階段掃描老年代的根的一部分。

在理解了這一點的基礎上,那么對於階段必須在下一次young GC啟動前完成的要求,也就理解了。因為如果第二次的young GC啟動了,那么這個過程中,survivor region就可能發生變化。這個時候執行root region phase就會產生錯誤的結果。

Concurrent marking phase

在標記階段,會使用到一個marking stack的東西。G1不斷從marking stack中取出引用,遞歸掃描整個堆里的對象圖,並且在bitmap上進行標記。這個遞歸過程采用的是深度遍歷,會不斷把對象的域入棧。

在並發標記階段,因為應用還在運行,所以可能會有引用變更,包括現有引用指向別的對象,或者刪除了一個引用,或者創建了一個新的對象等。G1采用的是使用SATB的並發標記算法。

在資料6中記錄了使用SATB的兩條原則:

  1. All accessible cells at the beginning of the garbage collection are eventually marked during the marked phase;
  2. Newly alocated cells during the garbage collection are never collected during the sweep phase of that garbage collection

在G1中,該算法的關鍵在於,如果在並發標記的時候,出現了引用修改(不包含新分配內存給對象),那么寫屏障會把這些引用的原始值捕獲下來,記錄在log buffer中。而后再處理。后續的所有的標記,都是從原來的值出發,而不是從新的值出發的。

SATB是一個邏輯上存在概念,在實際中並沒有任何真的實際的數據結構與之對應。叫這個名字,是因為,一旦進入了concurrent marking階段,那么該在該階段的運行過程中,即便應用修改了引用,但是因為SATB的寫屏障記錄下來了原始的值,在遍歷整個堆查找存活對象的時候,使用的依然是原來的值。這就是在邏輯上保持了一個snapshot at the beginning of concurrent marking phase。

在處理新創建的對象,G1采用了不同的方式。G1用了兩個TAMS變量了判斷新創建的對象。一個叫做previous TAMS,一個叫做next TAMS。位於兩者之間的對象就是新分配的對象。

並發標記階段,bitmap和TAMS的作用如圖:

  

該圖的詳細解釋如下:

  1. A是第一次marking cycle的initial marking階段。next bitmap尚未標記任何存活對象,而此時的previous TAMS被初始化為region內存地址起始值,next TAMS被初始化為top。top實際上就是一個region未分配區域和已分配區域的分界點;
  2. B是經過concurrent marking階段之后,進入了remark階段。此時存活對象的掃描已經完成了,因此next bitmap構造好了,剛好代表的是當下狀態中region中的內存使用情況。注意的是,此時top已經不再與next TAMS重合了,top和next TAMS之間的就是在前面標記階段之時,新分配的對象;
  3. C代表的是clean up階段。C和B比起來,next bitmap變成了previous bitmap,而在bitmap中標記為垃圾(也就是白色區域的)的對應的region的區域也被染成了淺灰色。這並不是指垃圾對象已經被清掃了,僅僅是標記出來了。同時next TAMS和previous TAMS也交換了角色;
  4. D代表的是下一個marking cycle的initial marking階段,該階段和A類似,next TAMS重新被初始化為top的值;
  5. EF就是BC的重復;

Remark phase

該階段是一個STW的階段。引入該階段的目的,是為了能夠達到結束標記的目標。要結束標記的過程,要滿足三個條件:

  1. concurrent marking已經追蹤了所有的存活對象;
  2. marking stack是空的;
  3. 所有的log都被處理了;

前兩個條件是很容易達到的,但是最后一個是很困難的。如果不引入一個STW的remark過程,那么應用會不斷的更新引用,也就是說,會不斷的產生log,因而永遠也無法達成完成標記的條件。

Clean up

該階段主要完成:

  1. 統計存活對象,這是利用RS和bitmap來完成的,統計的結果將會用來排序region,以用於下一次的CSet的選擇;
  2. 重置RSet;
  3. 把空閑region放到空閑region列表中;

該階段比較容易引起誤解地方在於,Clean up並不會清理垃圾對象,也不會執行存活對象的拷貝。也就是說,在極端情況下,該階段結束之后,空閑Region列表將毫無變化,JVM的內存使用情況也毫無變化。

Evacuation

Evacuation階段STW的,大概可以分成兩個步驟:第一個步驟是從Region中選出若干個Region進行回收,這些被選中的Region稱為Collect Set(簡稱CSet);而第二個步驟則是把這些Region中存活的對象復制到空閑的Region中去,同時把這些已經被回收的Region放到空閑Region列表中。
這兩個步驟又可以被分解成三個任務:

  1. 根據RS的日志更新RS:只有在處理完了RS的日志之后,RS才能夠保證是准確的,完整的,這也是Evacuation是STW的重要原因;
  2. 掃描RS和其余的根來確定存活對象:該階段實際上最主要依賴於RS;
  3. 拷貝存活對象:該階段只要從2中確定的根觸發,沿着引用鏈一直追溯下去,將存活對象復制到新的region就可以。這個過程中,可能有一部分的年輕代對象會被提升到老年代;

Evacuation的時機

Evacuation的觸發時機在不同的模式下會有一些不同。在不同的模式下都相同的是,只要堆的使用率達到了某個閾值,就必然會觸發Evacuation。這是為了確保在Evacuation的時候有足夠的空閑Region來容納存活對象。

在young GC的情況下,G1會選擇N個region作為CSet,該CSet首先需要滿足軟實時的要求,而一旦已經有N個region已經被分配了,那么就會執行一次Evacuation。

G1會盡可能的執行mixed GC。唯一的限制就是mix GC也需要滿足軟實時的要求。

G1觸發Evacuation的原則大概是:

  1. 如果被分配的young region數量滿足young GC的要求,那么就會觸發young GC;
  2. 如果被分配的young region數量不滿足young GC,就會進一步考察加上old region的數量,能否滿足old GC的要求;

為了理解這一點,可以舉例來說,假如回收一個old region的時間是回收一個young region的兩倍,也就是young region花費時間T,old region花費2T,在滿足軟實時目標的情況下,GC只能回收8T的region,那么:

  1. 假如應用現在只分配k(k<8)塊young region,沒有分配任何old region。這個時候又分配了一個old region,那么這個時候會立刻觸發一次mixed GC,此次GC會選擇k塊young region和一塊old region;
  2. 因此,在這種假設下,只要有可以回收的old region的時候,總是會先回收old region;
  3. 在沒有任何old region的情況下,才有可能觸發young region。

當然,在一般情況下,這些假設是不成立的。讀者可以思考一下,在young GC和mixed GC達到軟實時的要求下,young region和old region之間回收的花銷不同會導致young GC和mixed GC會在什么情況下觸發。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM