再回首CMS垃圾回收

前言

以前學習JVM垃圾回收時,主要是過了一遍垃圾收集算法,好比複製算法,標記-清除算法,標記-整理算法,在此基礎上能夠增長分代,每代採起不一樣的回收算法,以提升總體的分配和回收效率。而後過了一遍JVM中的垃圾收集器,好比Serial、Parallel Scavenge、Parallel New、CMS、G1等。java

自認爲垃圾收集就是根據GC Root標記全部可達的對象,而後把全部沒有標記的對象清除就ok了。是否是很簡單。事實上垃圾收集也就是這麼一回事,可是不少時候提及來簡單,作起來卻會出現不少問題。這篇文章就是記錄我對CMS垃圾收集器的一些疑問並學習的過程。算法

首先看一下CMS的總體流程(具體每一個流程的詳情就自行了解吧)數組

CMS流程

如何進行標記?

最近在看Golang的GC算法實現,裏面用到了三色標記法,可是在個人知識庫中對三色標記法有這個概念,是的,我只知道這個概念,不知道三色標記法是怎麼一個流程,也不知道三色標記法在GC中怎麼與運行的。因而就開始了個人探險之旅。markdown

在搜索了一下三色標記法(具體能夠看一下文末參考文檔中三色標記法與讀寫屏障瞭解詳情)後,發現現代追蹤式(可達性分析)的垃圾回收器幾乎都借鑑了三色標記的算法思想,CMS垃圾收集器也不例外。數據結構

GC Root有哪些?

咱們知道怎麼進行標記了,但最初標記的時候須要一些根據才行啊,這些根據就是咱們收的GC Root。GC Root有哪些?網上有不少的答案,個人理解就是併發

  • 當前活躍調用棧中的指向對象的引用
  • 一些不會發生改變的數據所指向的引用

這裏我使用的是引用,而不是對象,由於R大是這樣說的(具體的問題見參考文檔java的gc爲何要分代?oop

所謂「GC roots」,或者說tracing GC的「根集合」,就是一組必須活躍的引用。 例如說,這些引用可能包括:學習

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

注意,是一組必須活躍的引用,不是對象。spa

如今知道了GC Root,可是咱們都知道有分代的概念,新生代的gc和老年的代的gc回收的區域是不同,那麼這裏的GC Root是否是應該不同呢?確定是不同的。線程

首先看一下新生代的GC

新生代的區域通常都比較小,並且對象的存活率都比較低,因此按照前面說的GC Root在新生代的區域掃描就好了。可是會有一個問題?老年代存在引用新生代對象的可能啊?若是隻掃描新生代的區域,會漏掉被老年代引用的對象,這些對象就會被清除掉,這是不容許的。

若是這樣的話,那是否是掃描一下老年代的對象,看是否引用新生代的對象是否是就ok了?嗯這麼作確定是ok的,可是老年代通常很大,並且存活的對象不少,會致使掃描佔用很長的時間。那這個問題如何解?JVM是如何避免Minor GC時掃描全堆的?

通過統計信息顯示,老年代持有新生代對象引用的狀況不足1%,根據這一特性JVM引入了卡表(card table)來實現這一目的。以下圖所示:

CardTable.png

CardTable

卡表的具體策略是將老年代的空間分紅大小爲512B的若干張卡(card)。卡表自己是單字節數組,數組中的每一個元素對應着一張卡,當發生老年代引用新生代時,虛擬機將該卡對應的卡表元素設置爲適當的值。如上圖所示,卡表3被標記爲髒(卡表還有另外的做用,標識併發標記階段哪些塊被修改過),以後Minor GC時經過掃描卡表就能夠很快的識別哪些卡中存在老年代指向新生代的引用。這樣虛擬機經過空間換時間的方式,避免了全堆掃描。

因此新年代GC的GC Root包含2部分

  • 新生代中知足GC Root定義的對象
  • 卡表中老年代引用新生代的對象

老年代的GC

前面咱們說了新生代的gc,咱們已一樣的思路來看看老年代的gc,老年代的GC Root如何來標記呢?只掃描老年代能夠嗎?固然是不行的,由於新生代中也可能存在老年代對象的引用,好在新生代並不大,因此老年代GC的時候還須要掃描一遍新生代。

新生代GC的Root.png

因此老年代GC的GC Root包含2部分

  • 老生代中知足GC Root定義的對象,如圖節點1;
  • 標記年輕代中活着的對象引用到的老年代的對象(指的是年輕代中還存活的引用類型對象,引用指向老年代中的對象)如圖節點二、3;

併發標記的好壞?

標記做爲垃圾回收的第一步,如今知道如何進行標記,接下來就是遍歷這些對象,將全部未標記的對象清理就完成GC了。

然而事實上並無這麼簡單,若是標記的時候是STW的,那就是這麼簡單,可是若是標記過程都STW會形成暫停時間過長,給人的感受就是系統一卡一卡的。

因而就把標記的過程改爲併發的進行,也就是CMS中併發標記的過程,然而這就是一切複雜問題的源頭。雖然併發標記提高了標記的效率,可是所以卻引起了一系列的問題。

由於併發標記時,gc線程和用戶線程是並行的,因此在這個過程當中會出現下面的狀況(須要瞭解三色標記法與讀寫屏障):

  • 新生代晉升到老年代
  • 黑色對象取消對灰色對象的引用(浮動垃圾)
  • 黑色對象新增對白色對象的引用(漏標)

其實在三色標記法與讀寫屏障文中已經給出瞭解決方法--添加讀寫屏障

  • 寫屏障 + SATB
  • 寫屏障 + 增量更新
  • 讀屏障(Load Barrier)

在CMS併發標記階段,使用 寫屏障 + 增量更新 的方法,將上面出現的狀況標記爲dirty,這樣最後再遍歷處理一下Dirty集合中的對象就ok了

標記爲dirty

從新標記階段爲何還要掃描新生代?

由於存在跨代引用,可是前面說過這種狀況,經過讀寫屏障的方式標記這些爲dirty,只須要掃描老年代和dirty集合就好了啊?哎,看來我仍是太年輕,若是隻掃描老年代和dirty集合會漏掉一部分,會是哪部分呢?老年代和dirty集合尚未覆蓋完嗎?

是的,老年代和dirty集合的確沒有覆蓋完。咱們來分析一下。老年代中通過初始標記和併發標記後,只有黑色對象和白色對象了,黑色的就是要留下的,白色的就是要被清除的。黑色對象是怎麼來的?根據GC Root找到的,因此只要併發標記過程當中,GC Root不發生變化,黑色對象就沒有問題(不會漏標),若是在併發標記過程當中GC Root發生了變化呢?

當併發標記過程當中GC Root增長了,而且這個GC Root還引用了老年代中的對象,此時若是隻掃描老年代和dirty集合就會漏標。所以從新標記階段仍然須要掃描新生代。

預處理階段都幹了啥?

預處理階段其實有2部分:

  • 預清理階段
  • 可終止的預處理

這個階段的目的都是爲了減輕後面的從新標記的壓力,提早作一點從新標記階段的工做。通常CMS的GC耗時80%都在remark階段,因此預處理階段也是爲了減小remark階段的STW時間。

從新標記階段須要作一下工做:

  1. 遍歷新生代對象,從新標記
  2. 根據GC Roots,從新標記
  3. 遍歷老年代的Dirty Card,從新標記(這裏的Dirty Card大部分已經在clean階段處理過)

遍歷新生代對象時,可能不少對象已是不可達了,可是仍是須要掃描。遍歷Dirty Card作處理。

這2部分其實就是預處理階段幫助從新標記減輕壓力的地方

  • 預清理階段和可終止的預處理都會掃描Dirty Card作處理
  • 可終止的預處理,儘可能進行一次ygc,讓不可達的對象被回收掉,remark階段遍歷新生代的對象成本小一點

具體這個階段的詳情見參考文檔圖解CMS垃圾回收機制,你值得擁有

參考文檔

相關文章
相關標籤/搜索