Concurrent Mark Sweep(cms)垃圾回收器

        好長時間沒寫過博客了,突發奇想,開始寫下最近幾年的積累吧,先從Concurrent Mark Sweep(cms)開始,希望自己沒有太懶吧,堅持寫完吧,先介紹以下概念:

GC ROOT

這裏我引用下RednaxelaFX的原話,所謂“GC roots”,或者說tracing GC的“根集合”,就是一組必須活躍的引用(重點)。
例如說,這些引用可能包括:

  • 所有Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用;換句話說,當前所有正在被調用的方法的引用類型的參數/局部變量/臨時值。
  • VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如說HotSpot VM裏的Universe裏有很多這樣的引用。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被加載的Java類
  • (看情況)Java類的引用類型靜態變量
  • (看情況)Java類的運行時常量池裏的引用類型常量(String或Class類型)
  • (看情況)String常量池(StringTable)裏的引用

CARD TABLE

  • 基於卡表(Card Table)的設計,通常將堆空間劃分爲一系列2次冪大小的卡頁(Card Page)。
  • 卡表(Card Table),用於標記卡頁的狀態,每個卡表項對應一個卡頁。
  • HotSpot JVM的卡頁(Card Page)大小爲512字節,卡表(Card Table)被實現爲一個簡單的字節數組,即卡表的每個標記項 爲 1個字節。
  • 當對一個對象引用進行寫操作時(對象引用改變),寫屏障邏輯將會標記對象所在的卡頁爲dirty。
  • OpenJDK/Oracle 1.6/1.7/1.8 JVM默認的卡標記簡化邏輯如下:
CARD_TABLE [this address >> 9] = 0;
  • 首先,計算對象引用所在卡頁的卡表索引號。將地址右移9位,相當於用地址除以512(2的9次方)。可以這麼理解,假設卡表卡頁的起始地址爲0,那麼卡表項0、1、2對應的卡頁起始地址分別爲0、512、1024(卡表項索引號乘以卡頁512字節)。
  • 其次,通過卡表索引號,設置對應卡標識爲dirty。

Mod Union Table

  • 當一個card跨代(年輕代依賴老年代)引用,年輕代gc需要掃描這些dirty card,看是否有跨代引用,也可能由於跨代引用不存在了,年輕代會擦除這個dirty card的狀態,但是dirty card只有一份,年輕代gc和老年代gc都操作會產生誤操作,所以有了Mod Union Table,結構和card table基本一致。
  • 介紹完GCROOT,然後說下CMS的過程:

1:初始標記(stop the word)

  •  初始標記做的事情是二件:
  • ①:遍歷GCRoot可直達的老年代對象(圖中紅顏色字體的1)
  • ②:遍歷新生代直達的老年代對象  (圖中紅顏色字體的2和3)
  • 從上面的圖中也可以看出來,初始標記是做了部分年輕代GC的事情,這裏顯然是可以優化的,g1就優化了這個過程,每次老年代的GC發生在年輕代GC之後,這樣就省去了trace年輕代的過程,後面的帖子我會對比cms和g1設計上的不同和優化。

2:併發標記

 

併發標記和其名字一樣,併發執行,主要做二件事情:

  • ①:沿着初始標記的1,2,3對象,進行trace遍歷(4,5),直到所有對象被遍歷標記完全,(6,7,8,9,10)未被標記,將被回收。
  • ②:新生代晉升到老年代,直接在老年代分配的對象,還有老年代內部引用變化的對象,這些對象所在的card table被標記爲dirty,也就是髒卡(dirty card)。
  • 爲什麼會有這個操作,下面我介紹一下三色標記法:
  • 白色:還沒有搜索過的對象(白色對象會被當成垃圾對象)
  • 灰色:正在搜索的對象
  • 黑色:搜索完成的對象(不會當成垃圾對象,不會被 GC)

A.c = C;
B.c = null;
  • 如果灰色對象B下面的引用的白色對象c在併發階段,成爲了黑色對象A下面的引用,那麼會發生什麼事情?會產生漏標,c對象最終會被回收,這是非常可怕的事情,活着的對象被回收了,這是不能接受的,處理這種情況一般有二種方式:
  • ①:在對象引用發生變化之前記錄對象引用關係,灰色(B)對象斷開白色對象(C)的引用時記錄,保證不會漏標。(寫前屏障)(B.c = null)g1
  • ②:在對象引用發生變化之後記錄對象引用關係,黑色(A)對象新增白色(C)對象是記錄,保證了不會漏標。(寫後屏障)(A.c = c)cms
  • cms採用的是寫後屏障,增量更新(Incremental Update)

3:併發預清理

  • 通過參數CMSPrecleaningEnabled選擇關閉該階段,默認啓用:
  • ①:掃描併發標記階段老年代的Dirty Card,重新標記那些在併發標記階段引用被更新的對象

4:併發可中斷的預清理

  • CMS 有兩個參數:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默認值分別是2M、50%。兩個參數組合起來的意思是預清理後,eden空間使用超過2M時啓動可中斷的併發預清理(CMS-concurrent-abortable-preclean),直到eden空間使用率達到50%時中斷,進入remark階段。CMSMaxAbortablePrecleanTime=5s和循環次數(默認是0,不限制)CMSMaxAbortablePrecleanLoops也能控制退出。
  • ①:處理新生代(survivor不是eden)引用到的老年代的對象,modUnionTable等
  • 爲什麼會有這個階段?
  • 其實這個階段更多的作用是期望能夠發生一次minor gc(ParNew gc),因爲接下來的Final Remark階段要掃描整個的新生代,爲什麼要掃描新生代?因爲新生代的對象關係變化比較大,Dirty Card卡比較多,與其掃描新生代的Dirty Card,不如直接掃描整個年輕代,但是如果新生代太大,掃描起來太費時間,就會得不償失,Final Remark(stop the world)的時間會很長,所以期望發生一次minor gc回收年輕代,但也僅僅是期望,很多人認爲CMSScheduleRemarkEdenPenetration=50%;永遠也到不了100%,不可能觸發minor gc,下面切一段代碼:
 while (!(should_abort_preclean() ||
             ConcurrentMarkSweepThread::should_terminate())) {
      workdone = preclean_work(CMSPrecleanRefLists2, CMSPrecleanSurvivors2);
      cumworkdone += workdone;
      loops++;
      // Voluntarily terminate abortable preclean phase if we have
      // been at it for too long.
      if ((CMSMaxAbortablePrecleanLoops != 0) &&
          loops >= CMSMaxAbortablePrecleanLoops) {
        if (PrintGCDetails) {
          gclog_or_tty->print(" CMS: abort preclean due to loops ");
        }
        break;
      }
      if (pa.wallclock_millis() > CMSMaxAbortablePrecleanTime) {
        if (PrintGCDetails) {
          gclog_or_tty->print(" CMS: abort preclean due to time ");
        }
        break;
      }
      // If we are doing little work each iteration, we should
      // take a short break.
      if (workdone < CMSAbortablePrecleanMinWorkPerIteration) {
        // Sleep for some time, waiting for work to accumulate
        stopTimer();
        cmsThread()->wait_on_cms_lock(CMSAbortablePrecleanWaitMillis);
        startTimer();
        waited++;
      }
    }
  • 因爲preclean_work標記需要時間,如果你的對象增長很快,還沒有來得及下個while開始,對象已經填滿eden區,就會發生minor gc,如果這個時間剛好是CMSMaxAbortablePrecleanTime=5s,那麼大家都高興,但是有時候事實和我們有誤差,用的時間還差5秒很多,這時候eden區的內存增長到50%退出,這個50%是爲了避免二次minor gc,長時間的停頓,所以我們的minor gc實際上這種就沒有太大的幫助,所以這二個參數CMSMaxAbortablePrecleanTime和CMSScheduleRemarkEdenPenetration是可以優化,我們也可以加上CMSScavengeBeforeRemark這個參數進行final remark前的優化,進行一次minor gc,出現這種情況畢竟計算機硬件發展太快,以前的默認參數,不一定符合現在的情況,jdk5出來已經很多年了。

5:最終標記(stop the word)

  • 由於上一個過程也是併發的,不可能所有對象都能被標記到,這個階段就stop the world,查缺補漏,包含上面幾個過程的全部內容
  • ①:遍歷年輕代作爲根標記老年代對象包括modUnionTable。
  • ②:遍歷dirty card標記老年代對象。
  • 這個過程很多人說不是重複了麼,實際上,gcroot trace的過程,遇到被標記的第一個元素,就會終止,上面的過程並不多餘。

6:併發清理

  • 如圖,6,7,8,9就被清理掉了,這個階段是併發的,但是效率並不是特別高,由於是並行的,還會產生浮動垃圾,就是對象變成不可達了,但是標記已經結束了,沒法在標記了,就產生了浮動垃圾,g1的時候就變成了stop the word了。

7:併發重置

  • 最後一個階段,重置cms的數據結構

 

到這裏基本上可以結束了,但是總得湊下字數吧,整個的gc過程我大概說一下:年輕代minor gc之後,對象晉升到老年代,老年代的對象越來越多之後,就會觸及閾值,就會觸發cms gc;如果這個時候新對象來到老年代,老年代沒有足夠空間(Concurrent Mode Failure),就會觸發serial old 做擔保的full gc,如果full gc之後仍沒有空間,就會觸發oom,下一篇我就會說一個案例,如何分析的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章