CMS垃圾收集器運行原理

目錄

概述

執行過程

初始標記initial mark

併發標記 concurrent mark

三色表示

遍歷算法

標記過程舉例

併發過程中變化的維護

併發預清理concurrent preclean

可中斷預清理concurrent abortable preclean

重新標記final remark

併發清除concurrent sweep

併發重置concurrent reset

缺點

cpu資源敏感,降低吞吐量

浮動垃圾

內存碎片

運行過程常見問題

concurrent mode failure

promotion failed

OutOfMemoryError 


 

 

概述

CMS垃圾收集器是一款優秀的老年代併發垃圾收集器,通過與用戶線程併發執行的方式減少GC停頓的時間。本文主要聊一下CMS設計到的相關的數據結構、具體的執行過程、運行中會出現的異常情況。

在CMS之前並行垃圾收集器通過下圖方式進行,雖然GC階段多線程並行執行單此時用戶線程是完全暫停的。如果GC時間過長,將引發服務響應超時、調用接口超時等各類異常。

而CMS垃圾收集器大部分時間GC線程與用戶線程併發執行,只有在初始標記和重新標記階段才暫停用戶線程

總體思路:當達到GC條件時,開始併發標記存活的對象,併發的過程中記錄對象引用關係的變化。併發標記結束後,暫停用戶線程,處理引用關係變化,得到所有存活的對象和可以清理的對象。最終併發清理掉不被使用的對象(存在已經不被引用但本次清理不掉的對象)。

執行過程

初始標記initial mark

暫停用戶線程,標記GC root和新生代直接關聯的對象。

這裏的GC root關聯的對象包含虛擬機棧中引用的對象、類靜態屬性引用的對象、本地方法棧中JNI引用的對象。另外,CMS是老年代的垃圾收集器,被新生代引用的對象應該被標記爲存活,所以這裏還包含新生代對象。

併發標記 concurrent mark

啓動用戶線程,三色遍歷法遍歷標記老年代的所有對象。

三色表示

  • 黑色對象:自己被標記,且引用對象已經處理完成。
  • 灰色對象:自己被標記,但引用對象未處理。
  • 白色對象:沒有對它做標記。

     標記過的意思就是認爲這個對象是存活的,本次GC不回收這個對象。

遍歷算法

說到對象的遍歷方式,自然而然會想到藉助棧或隊列進行圖遍歷。對象之間的關係正好構建成一個圖,對象就是節點,引用關係就是邊。但是圖遍歷有這樣一個問題: GC root和新生代的對象都要裝進棧或隊列當中,可能會導致佔用特別大的額外空間。

爲了避免上的問題,CMS採用了線性遍歷的方式。

首先,引入一個bit數組,用它表示內存中每個位置的狀態,每個bit位代表4字節的內存空間,所以它的大小是固定的。

遍歷的策略也直接引用論文中的圖片,感興趣的同學可以細品~

遍歷bitmap,找到被標記爲存活的對象cur,將它壓入棧中,然後開始遍歷這個對象出發的所有對象。遍歷棧的過程中,如果遇到地址比cur低的對象則標記並壓如棧中,遇到地址比cur高則只標記不入棧。所有GC root引用的對象已經在初始標記階段標記成了存活對象,遍歷過程遇到其中一個就開始利用棧遍歷它及它之前的所有對象,這就保證了棧中最多有1個GC root直接引用的對象,有效控制了棧空間的大小。

標記過程舉例

標記的過程伴隨着對象引用的修改,下圖舉例說明了併發標記的過程

  • 初始標記(圖中未體現,參考步驟a):初始標記時對a進行了標記。
  • 步驟a:對a引用的對象bc,及b引用的對象e進行標記,根據當時的對象引用關係,abcegd是存活的。
  • 步驟b:對象引用關係發生了改變,b不再引用c新增引用d,g不再引用d
  • 步驟c:完成了併發標記的過程,abceg被標記。第二個和第四個區域內的對象引用關係發生了改變,被記錄了下來。這裏面運用到了card table、mod union table數據結構和write barrier技術,後面進行說明。
  • 步驟d:實際上是重新標記後的結果,可以看到對象d在併發標記結束時未進行標記,但是它還在被對象b引用,不應該回收。這就依賴重新標記階段對dirty card(對象引用關係發生變化的區域)的處理。

從上面的例子中可以看到CMS的一些特性:

  • 初始標記時,不可達的對象,在本次GC中一定會被回收,如對象f
  • 已經標記過的對象,即使最終已經不被引用本次GC也不會回收,如對象c

併發過程中變化的維護

card table與mod union table

card table是一個數組,數組中每個位置存的是一個byte,每個比特位有不同的作用(具體的作用可能得研究下源碼,沒有找到相關的資料)。CMS將老年代的空間分成大小爲512bytes的塊,card table中的每個元素對應着一個塊。

對於新生代:它記錄老年代到新生代的引用,younggc時不用遍歷整個老年代

對於老年代:它記錄併發標記開始引用發生變化的card,併發標記結束後需要處理這些card

由於新生代GC與老年代GC同時使用card table,所以會出現衝突的情況。新生代GC時,發現老年代的dirty card(card的一種狀態)沒有指向新生代的引用,會將這個card設置爲clean(改變了老年代對象引用發生設置的狀態),但這個card必須在remark階段進行重新標記。所以增加了另一個數據結構mod union table解決此問題。

mod union table是一個bit位向量,一個bit表示一個card的狀態。

它由新生代垃圾收集器維護,新生代GC將card設置爲clean之前,把mod union table設置爲dirty。card table狀態爲dirty、或者mod union table標記爲dirty、或者同時兩種數據結構都標記爲dirty的card表示併發標記階段引用發生了變化,需要在後面的階段進行處理。

write barrie 

write barrie寫屏障類似於一個切面,用戶線程寫對象引用的時候就觸發write barrier的邏輯,將對象所處的card設置爲dirty。

併發預清理concurrent preclean

處理dirty card,降低remark階段暫停時間。

重新標記的過程是STW的,所以爲了縮短停頓時間,在併發標記之前應該儘可能多的完成重新標記階段的工作。併發預清理就是對dirty card進行遍歷處理,降低重新標記需要處理的dirty card的數量。

可中斷預清理concurrent abortable preclean

繼續處理dirty card,滿足條件時停止當前階段,進入下一階段。

可中斷預清理也會處理dirty card,替remark階段分擔一部分工作。這個階段更主要的目的是控制remark和新生代GC分開執行,避免連續兩次暫停導致總的暫停時間過長,其中運用了一些策略。

預清理階段結束之後,如果Eden空間大於CMSScheduleRemarkEdenSizeThreshold(默認2M),則進入可中斷預清理階段。

當Eden空間達到CMSScheduleRemarkEdenPenetration(默認50%)時進入remark階段。

如果等待超過了CMSMaxAbortablePrecleanTime(默認5s)同樣進入remark階段。

另外,還有個CMSMaxAbortablePrecleanLoops參數可以控制可中斷預清理循環的次數,到達次數則退出預清理階段進入remark,默認是0不限制次數。

重新標記final remark

暫停用戶線程,從GC root(包含新生代對象)出發重新標記,並處理完所有dirty card。

併發階段,老年代可能面對如下變化,重新標記後全部確定哪些是存活的或者不存活的

  • 晉升到老年代的對象
  • 直接分配到老年代的對象
  • 老年代中引用關係改變的對象
  • 新生代到老年代引用關係的改變
  • GC root到老年代引用關係的改變

新生代大小的影響和控制

重新標記階段需要遍歷新生代對象,但新生代裏大多都是垃圾,如果remark之前發生一次新生代GC,則會大大減小remark階段需要遍歷的對象數量。可以設置CMSScavengeBeforeRemark參數強制在remark之前執行一次新生代GC。但是正如可中斷預清理階段的分析,新生代GC也是有停頓的,這樣兩次停頓連在一起也可能會很長,需要進行權衡。

併發清除concurrent sweep

併發清除標記爲不可達的對象,回收併合並空閒內存。

併發重置concurrent reset

重新設置CMS相關的各種狀態及數據結構,爲下一個垃圾收集週期做好準備。

缺點

cpu資源敏感,降低吞吐量

CMS沒有運行的時候所有全部cpu資源都供用戶線程使用,CMS開始併發運行後就要跟用戶線程競爭cpu資源,導致應用線程運行變慢。對於cpu資源非常緊缺的系統,假設只有2核,CMS運行起來後將佔用一半的cpu資源,用戶線程將感知到運行速度減半。

併發帶來的好處是可以降低用戶線程的停頓時間,對於在線服務類應用非常有益,因爲長時間的停頓可能導致響應超時等問題。但相對於非併發垃圾收集器,CMS整個週期內很多工作是重複的(比如重新標記階段對dirty card中的對象重新標記,而在併發標記階段可能已經標記過了),導致整體的吞吐量是降低的。

浮動垃圾

因爲CMS垃圾收集器的特性,被標記過的對象,即使最終變成垃圾本次GC也不會回收它,這些垃圾就是浮動垃圾。浮動垃圾的產生意味着內存裏不光裝着存活對象,還要裝着這些浮動垃圾,所以容納同樣多的存活對象CMS需要佔用更大的內存空間。

內存碎片

CMS使用標記清除算法,收集結束之後會產生大量內存碎片。當有大對象需要分配空間時,可能總的空間大小是足夠的,但是沒有連續的空間裝下此對象。

CMS默認開啓UseCMSCompactAtFullCollection 參數,在FullGC時進行內存碎片的合併整理。內存碎片雖然解決了,但負面影響就是停頓時間變長了。還有另外一個CMSFullGCsBeforeCompaction參數可以控制多少次FullGC纔會進行整理,默認是0代表每次FullGC都會進行碎片整理。

運行過程常見問題

concurrent mode failure

併發雖好,但會引入一些問題。對於非併發的垃圾收集器,可以等到老年代無法分配對象時再執行GC。但對於cms則需要預留出空間提前開始GC,預留的空間供併發期間新對象的分配及新生代對象的晉升使用。如果在老年代分配對象發現老年代裝不下,則會觸發concurrent mode failure,此時將會暫停用戶線程執行FullGC或者串行模式的CMS。

promotion failed

這個錯誤涉及到CMS擔保機制,新生代GC之前會根據歷史晉升到老年代對象的大小,預估本次老年代是否足夠容納新生代晉升的對象。如果預估時空間足夠,但新生代GC實際執行時發現容納不了,則會引起promotion failed錯誤。

OutOfMemoryError 

CMS垃圾收集器發現大部分時間都浪費在GC上就會拋出OutOfMemoryError異常,具體爲98%的時間在GC但回收不到2%的空間。這樣做實際上是爲了防止程序進入一種雖然在運行實際上一直在GC假死狀態,也可以通過設置-XX:-UseGCOverheadLimit禁用該機制。

 

閱讀建議:不管是大V或是小號都會有自己的關注點、忽略的點,都有自己理解到位以及理解有偏差的地方。如果有緣讀到本文,建議參考下官方文檔、論文。至於源碼,閱讀成本比較高,感覺看一下自己感興趣的地方就可以了,網上有一些源碼理解的分享。

官網:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html

論文:https://www.cs.purdue.edu/homes/hosking/ismm2000/papers/printezis.pdf

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