不可錯過的CMS學習筆記

引子

帶着問題去學習一個東西,纔會有目標感,我先把一直以來本身對CMS的一些疑惑羅列了下,但願這篇學習筆記能解決掉這些疑惑,但願也能對你有所幫助。html

  1. CMS出現的初衷、背景和目的?
  2. CMS的適用場景?
  3. CMS的trade-off是什麼?優點、劣勢和代價
  4. CMS會回收哪一個區域的對象?
  5. CMS的GC Roots包括那些對象?
  6. CMS的過程?
  7. CMS和Full gc是否是一回事?
  8. CMS什麼時候觸發?
  9. CMS的日誌如何分析?
  10. CMS的調優如何作?
  11. CMS掃描那些對象?
  12. CMS和CMS collector的區別?
  13. CMS的推薦參數設置?
  14. 爲何ParNew能夠和CMS配合使用,而Parallel Scanvenge不能夠?

1、基礎知識

  1. CMS收集器:Mostly-Concurrent收集器,也稱併發標記清除收集器(Concurrent Mark-Sweep GC,CMS收集器),它管理新生代的方式與Parallel收集器和Serial收集器相同,而在老年代則是儘量得併發執行,每一個垃圾收集器週期只有2次短停頓。
  2. 我以前對CMS的理解,覺得它是針對老年代的收集器。今天查閱了《Java性能優化權威指南》和《Java性能權威指南》兩本書,確認以前的理解是錯誤的。
  3. CMS的初衷和目的:爲了消除Throught收集器和Serial收集器在Full GC週期中的長時間停頓。
  4. CMS的適用場景:若是你的應用須要更快的響應,不但願有長時間的停頓,同時你的CPU資源也比較豐富,就適合適用CMS收集器。

2、CMS的過程

CMS的正常過程java

這裏咱們首先看下CMS併發收集週期正常完成的幾個狀態。性能優化

  1. (STW)初始標記:這個階段是標記從GcRoots直接可達的老年代對象、新生代引用的老年代對象,就是下圖中灰色的點。這個過程是單線程的(JDK7以前單線程,JDK8以後並行,能夠經過參數CMSParallelInitialMarkEnabled調整)。初始標記標記的對象
  2. 併發標記:由上一個階段標記過的對象,開始tracing過程,標記全部可達的對象,這個階段垃圾回收線程和應用線程同時運行,如上圖中的灰色的點。在併發標記過程當中,應用線程還在跑,所以會致使有些對象會重新生代晉升到老年代、有些老年代的對象引用會被改變、有些對象會直接分配到老年代,這些受到影響的老年代對象所在的card會被標記爲dirty,用於從新標記階段掃描。這個階段過程當中,老年代對象的card被標記爲dirty的可能緣由,就是下圖中綠色的線:併發標記過程當中受到影響的對象
  3. 預清理:預清理,也是用於標記老年代存活的對象,目的是爲了讓從新標記階段的STW儘量短。這個階段的目標是在併發標記階段被應用線程影響到的老年代對象,包括:(1)老年代中card爲dirty的對象;(2)倖存區(from和to)中引用的老年代對象。所以,這個階段也須要掃描新生代+老年代。【PS:會不會掃描Eden區的對象,我看源代碼猜想是沒有,還須要繼續求證】預清理中掃描from和to區
  4. 可中斷的預清理:這個階段的目標跟「預清理」階段相同,也是爲了減輕從新標記階段的工做量。可中斷預清理的價值:在進入從新標記階段以前儘可能等到一個Minor GC,儘可能縮短從新標記階段的停頓時間。另外可中斷預清理會在Eden達到50%的時候開始,這時候離下一次minor gc還有半程的時間,這個還有另外一個意義,即避免短期內連着的兩個停頓,以下圖資料所示:避免連續停頓的發生服務器

    在預清理步驟後,若是知足下面兩個條件,就不會開啓可中斷的預清理,直接進入從新標記階段:數據結構

    • Eden的使用空間大於「CMSScheduleRemarkEdenSizeThreshold」,這個參數的默認值是2M;
    • Eden的使用率大於等於「CMSScheduleRemarkEdenPenetration」,這個參數的默認值是50%。
若是不知足上面兩個條件,則進入可中斷的預清理,可中斷預清理可能會執行屢次,那麼退出這個階段的出口有兩個(源碼參見下圖):

*   設置了CMSMaxAbortablePrecleanLoops,而且執行的次數超過了這個值,這個參數的默認值是0;
*   CMSMaxAbortablePrecleanTime,執行可中斷預清理的時間超過了這個值,這個參數的默認值是5000毫秒。![可中斷預清理退出的條件](https://upload-images.jianshu.io/upload_images/44770-d97c4aa0fb169f68.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 "可中斷預清理退出的條件")

    若是是由於這個緣由退出,gc日誌打印以下:![可中斷預清理因爲時間退出](https://upload-images.jianshu.io/upload_images/44770-6bf00ed16fbb9b9a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 "可中斷預清理因爲時間退出")

有可能可中斷預清理過程當中一直沒等到Minor gc,這時候進入從新標記階段的話,新生代還有不少活着的對象,就回致使STW變長,所以CMS還提供了*CMSScavengeBeforeRemark*參數,能夠在進入從新標記以前強制進行依次Minor gc。
  1. (STW)從新標記:從新掃描堆中的對象,進行可達性分析,標記活着的對象。這個階段掃描的目標是:新生代的對象 + Gc Roots + 前面被標記爲dirty的card對應的老年代對象。若是預清理的工做沒作好,這一步掃描新生代的時候就會花不少時間,致使這個階段的停頓時間過長。這個過程是多線程的。
  2. 併發清除:用戶線程被從新激活,同時將那些未被標記爲存活的對象標記爲不可達;
  3. 併發重置:CMS內部重置回收器狀態,準備進入下一個併發回收週期。

CMS的異常狀況多線程

上面描述的是CMS的併發週期正常完成的狀況,可是還有幾種CMS併發週期失敗的狀況:併發

  1. 併發模式失敗(Concurrent mode failure):CMS的目標就是在回收老年代對象的時候不要中止所有應用線程,在併發週期執行期間,用戶的線程依然在運行,若是這時候若是應用線程向老年代請求分配的空間超過預留的空間(擔保失敗),就回觸發concurrent mode failure,而後CMS的併發週期就會被一次Full GC代替——中止所有應用進行垃圾收集,並進行空間壓縮。若是咱們設置了UseCMSInitiatingOccupancyOnlyCMSInitiatingOccupancyFraction參數,其中CMSInitiatingOccupancyFraction的值是70,那預留空間就是老年代的30%。
  2. 晉升失敗:新生代作minor gc的時候,須要CMS的擔保機制確認老年代是否有足夠的空間容納要晉升的對象,擔保機制發現不夠,則報concurrent mode failure,若是擔保機制判斷是夠的,可是實際上因爲碎片問題致使沒法分配,就會報晉升失敗。
  3. 永久代空間(或Java8的元空間)耗盡,默認狀況下,CMS不會對永久代進行收集,一旦永久代空間耗盡,就回觸發Full GC。

3、CMS的調優

  1. 針對停頓時間過長的調優
    首先須要判斷是哪一個階段的停頓致使的,而後再針對具體的緣由進行調優。使用CMS收集器的JVM可能引起停頓的狀況有:(1)Minor gc的停頓;(2)併發週期裏初始標記的停頓;(3)併發週期裏從新標記的停頓;(4)Serial-Old收集老年代的停頓;(5)Full GC的停頓。其中併發模式失敗會致使第(4)種狀況,晉升失敗和永久代空間耗盡會致使第(5)種狀況。
  2. 針對併發模式失敗的調優oracle

    • 想辦法增大老年代的空間,增長整個堆的大小,或者減小年輕代的大小
    • 以更高的頻率執行後臺的回收線程,即提升CMS併發週期發生的頻率。設置UseCMSInitiatingOccupancyOnlyCMSInitiatingOccupancyFraction參數,調低CMSInitiatingOccupancyFraction的值,可是也不能調得過低,過低了會致使過多的無效的併發週期,會致使消耗CPU時間和更多的無效的停頓。一般來說,這個過程須要幾個迭代,可是仍是有必定的套路,參見《Java性能權威指南》中給出的建議,摘抄以下:框架

      > 對特定的應用程序,該標誌的更優值能夠根據 GC 日誌中 CMS 週期首次啓動失敗時的值獲得。具體方法是,在垃圾回收日誌中尋找併發模式失效,找到後再反向查找 CMS 週期最近的啓動記錄,而後根據日誌來計算這時候的老年代空間佔用值,而後設置一個比該值更小的值。
    • 增多回收線程的個數jvm

      CMS默認的垃圾收集線程數是*(CPU個數 + 3)/4*,這個公式的含義是:當CPU個數大於4個的時候,垃圾回收後臺線程至少佔用25%的CPU資源。舉個例子:若是CPU核數是1-4個,那麼會有1個CPU用於垃圾收集,若是CPU核數是5-8個,那麼久會有2個CPU用於垃圾收集。
  3. 針對永久代的調優
    若是永久代須要垃圾回收(或元空間擴容),就會觸發Full GC。默認狀況下,CMS不會處理永久代中的垃圾,能夠經過開啓CMSPermGenSweepingEnabled配置來開啓永久代中的垃圾回收,開啓後會有一組後臺線程針對永久代作收集,須要注意的是,觸發永久代進行垃圾收集的指標跟觸發老年代進行垃圾收集的指標是獨立的,老年代的閾值能夠經過CMSInitiatingPermOccupancyFraction參數設置,這個參數的默認值是80%。開啓對永久代的垃圾收集只是其中的一步,還須要開啓另外一個參數——CMSClassUnloadingEnabled,使得在垃圾收集的時候能夠卸載不用的類。

4、CMS的trade-off是什麼?

  1. 優點

    • 低延遲的收集器:幾乎沒有長時間的停頓,應用程序只在Minor gc以及後臺線程掃描老年代的時候發生極其短暫的停頓。
  2. 劣勢

    • 更高的CPU使用:必須有足夠的CPU資源用於運行後臺的垃圾收集線程,在應用程序線程運行的同時掃描堆的使用狀況。【PS:如今服務器的CPU資源基本不是問題,這個點能夠忽略】
    • CMS收集器對老年代收集的時候,再也不進行任何壓縮和整理的工做,意味着老年代隨着應用的運行會變得碎片化;碎片過多會影響大對象的分配,雖然老年代還有很大的剩餘空間,可是沒有連續的空間來分配大對象,這時候就會觸發Full GC。CMS提供了兩個參數來解決這個問題:(1)UseCMSCompactAtFullCollection,在要進行Full GC的時候進行內存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不壓縮的Full GC後,執行一次帶壓縮的Full GC。
    • 會出現浮動垃圾;在併發清理階段,用戶線程仍然在運行,必須預留出空間給用戶線程使用,所以CMS比其餘回收器須要更大的堆空間。

5、幾個問題的解答

  1. 爲何ParNew能夠和CMS配合使用,而Parallel Scanvenge不能夠?
    答:這個跟Hotspot VM的歷史有關,Parallel Scanvenge是不在「分代框架」下開發的,而ParNew、CMS都是在分代框架下開發的。
  2. CMS中minor gc和major gc是順序發生的嗎?
    答:不是的,能夠交叉發生,即在併發週期執行過程當中,是能夠發生Minor gc的,這個找個gc日誌就能夠觀察到。
  3. CMS的併發收集週期合適觸發?
    由下圖能夠看出,CMS 併發週期觸發的條件有兩個:觸發cms併發週期的條件

    • 閾值檢查機制:老年代的使用空間達到某個閾值,JVM的默認值是92%(jdk1.5以前是68%,jdk1.6以後是92%),或者能夠經過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個參數來設置;這個參數的設置須要看應用場景,設置得過小,會致使CMS頻繁發生,設置得太大,會致使過多的併發模式失敗。例如
    • 動態檢查機制:JVM會根據最近的回收歷史,估算下一次老年代被耗盡的時間,快到這個時間的時候就啓動一個併發週期。設置UseCMSInitiatingOccupancyOnly這個參數能夠將這個特性關閉。
  4. CMS的併發收集週期會掃描哪些對象?會回收哪些對象?
    答:CMS的併發週期只會回收老年代的對象,可是在標記老年代的存活對象時,可能有些對象會被年輕代的對象引用,所以須要掃描整個堆的對象。
  5. CMS的gc roots包括哪些對象?
    答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(參見R大對GC roots的概念的解釋);第二,CMS的併發收集週期中,如何判斷老年代的對象是活着?咱們前面提到了,在CMS的併發週期中,僅僅掃描Gc Roots直達的對象會有遺漏,還須要掃描新生代的對象。以下圖中的藍色字體所示,CMS中的年輕代和老年代是分別收集的,所以在判斷年輕代的對象存活的時候,須要把老年代看成本身的GcRoots,這時候並不須要掃描老年代的所有對象,而是使用了card table數據結構,若是一個老年代對象引用了年輕代的對象,則card中的值會被設置爲特殊的數值;反過來判斷老年代對象存活的時候,也須要把年輕代看成本身的Gc Roots,這個過程咱們在第三節已經論述過了。老年代和新生代互相做爲Gc Roots
  6. 若是個人應用決定使用CMS收集器,推薦的JVM參數是什麼?我本身的應用使用的參數以下,是根據PerfMa的xxfox生成的,你們也可使用這個產品調優本身的JVM參數:

    -Xmx4096M -Xms4096M -Xmn1536M 
    -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M 
    -XX:+UseConcMarkSweepGC 
    -XX:+UseCMSInitiatingOccupancyOnly 
    -XX:CMSInitiatingOccupancyFraction=70 
    -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 
    -XX:+CMSClassUnloadingEnabled 
    -XX:+ParallelRefProcEnabled 
    -XX:+CMSScavengeBeforeRemark 
    -XX:ErrorFile=/home/admin/logs/xelephant/hs_err_pid%p.log 
    -Xloggc:/home/admin/logs/xelephant/gc.log 
    -XX:HeapDumpPath=/home/admin/logs/xelephant 
    -XX:+PrintGCDetails 
    -XX:+PrintGCDateStamps 
    -XX:+HeapDumpOnOutOfMemoryError
  7. CMS相關的參數總結(須要注意的是,這裏我沒有考慮太多JDK版本的問題,JDK1.7和JDK1.8這些參數的配置,有些默認值可能不同,具體使用的時候還須要根據具體的版原本確認怎麼設置)

    | 編號 | 參數名稱 | 解釋 |
    | --- | --- | --- |
    | 1 | UseConcMarkSweepGC | 啓用CMS收集器 |
    | 2 | UseCMSInitiatingOccupancyOnly | 關閉CMS的動態檢查機制,只經過預設的閾值來判斷是否啓動併發收集週期 |
    | 3 | CMSInitiatingOccupancyFraction | 老年代空間佔用到多少的時候啓動併發收集週期,跟UseCMSInitiatingOccupancyOnly一塊兒使用 |
    | 4 | ExplicitGCInvokesConcurrentAndUnloadsClasses | 將System.gc()觸發的Full GC轉換爲一次CMS併發收集,而且在這個收集週期中卸載     Perm(Metaspace)區域中不須要的類 |
    | 5 | CMSClassUnloadingEnabled | 在CMS收集週期中,是否卸載類 |
    | 6 | ParallelRefProcEnabled | 是否開啓併發引用處理 |
    | 7 | CMSScavengeBeforeRemark | 若是開啓這個參數,會在進入從新標記階段以前強制觸發一次minor gc |

參考資料

  1. 從實際案例聊聊Java應用的GC優化
  2. 理解CMS垃圾回收日誌
  3. 圖解CMS垃圾回收機制,你值得擁有
  4. 爲何CMS雖然是老年代的gc,但仍要掃描新生代的?
  5. R大對GC roots的概念的解釋
  6. Introduce to CMS Collector
  7. 《深刻理解Java虛擬機》
  8. 《Java性能權威指南》
  9. Oracle的GC調優手冊
  10. what-is-the-threshold-for-cms-old-gc-to-be-triggered
  11. Frequently Asked Questions about Garbage Collection in the Hotspot Java VirtualMachine
  12. Java SE HotSpot at a Glance
  13. xxfox:PerfMa的參數調優神器
  14. 詳解CMS垃圾回收機制
  15. ParNew和PSYoungGen和DefNew是一個東西麼?
  16. Java SE的內存管理白皮書
  17. Garbage Collection in Elasticsearch and the G1GC
  18. A Heap of Trouble
  19. 畢玄的文章:爲何不建議
  20. JVM源碼分析之SystemGC徹底解讀

讀者討論

  1. 關於CMS收集器的回收範圍,下面這張圖是有誤導的,從官方文檔上看來,CMS收集器包括年輕代和老年代的收集,只不過對年輕代的收集的策略和ParNew相同,這個能夠從參考資料16的第11頁看到。

  2. concurrent mode failure和promotion failed觸發的Full GC有啥不一樣?(這個問題是我、阿飛、蔣曉峯一塊兒討論的結果)
    答:concurrent mode failure觸發的"Full GC"不是咱們常說的Full GC——正常的Full GC實際上是整個gc過程包括ygc和cms gc。也就是說,這個問題自己是有問題的,concurrent mode failure的時候觸發的並非咱們常說的Full GC。而後再去討論一個遺漏的知識點:CMS gc的併發週期有兩種模式:foreground和background。

    • concurrent mode failure觸發的是foreground模式,會暫停整個應用,會將一些並行的階段省掉作一次老年代收集,行爲跟Serial-Old的同樣,至於在這個過程當中是否須要壓縮,則須要看三個條件:(1)咱們設置了UseCMSCompactAtFullCollectionCMSFullGCsBeforeCompaction,前者設置爲true,後者默認是0,前者表示是在Full GC的時候執行壓縮,後者表示是每隔多少個進行壓縮,默認是0的話就是每次Full GC都壓縮;(2)用戶調用了System.gc(),並且DisableExplicitGC沒有開啓;(3)young gen報告接下來若是作增量收集會失敗。image.png
    • promotion failed觸發的是咱們常說的的Full GC,對年輕代和老年代都會回收,並進行整理。
  3. promotion failed和concurrent mode failure的觸發緣由有啥不一樣?

    • promotion failed是說,擔保機制肯定老年代是否有足夠的空間容納新來的對象,若是擔保機制說有,可是真正分配的時候發現因爲碎片致使找不到連續的空間而失敗;
    • concurrent mode failure是指併發週期還沒執行完,用戶線程就來請求比預留空間更大的空間了,即後臺線程的收集沒有遇上應用線程的分配速度。
  4. 什麼狀況下才選擇使用CMS收集器呢?我以前的觀念是:小於8G的都用CMS,大於8G的選擇G1。蔣曉峯跟我討論了下這個觀念,提出了一些別的想法,我以爲也有道理,記錄在這裏:

    • 除了看吞吐量和延時,還須要看具體的應用,比方說ES,Lucene和G1是不兼容的,所以默認的收集器就是CMS,具體見可參考資料17和18。
    • 小於3G的堆,若是不是對延遲有特別高的需求,不建議使用CMS,主要是因爲CMS的幾個缺點致使的:(1)併發週期的觸發比例很差設置;(2)搶佔CPU時間;(3)擔保判斷致使YGC變慢;(4)碎片問題,更詳細的討論參見資料19。



本文做者:杜琪

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索