不可錯過的CMS學習筆記

引子

帶著問題去學習一個東西,才會有目標感,我先把一直以來自己對CMS的一些疑惑羅列了下,希望這篇學習筆記能解決掉這些疑惑,希望也能對你有所幫助。

  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. 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收集器。

二、CMS的過程

CMS的正常過程

這裡我們首先看下CMS併發收集週期正常完成的幾個狀態。

1、(STW)初始標記:這個階段是標記從GcRoots直接可達的老年代對象、新生代引用的老年代對象,就是下圖中灰色的點。這個過程是單線程的(JDK7之前單線程,JDK8之後並行,可以通過參數CMSParallelInitialMarkEnabled調整)。

不可錯過的CMS學習筆記

2、併發標記:由上一個階段標記過的對象,開始tracing過程,標記所有可達的對象,這個階段垃圾回收線程和應用線程同時運行,如上圖中的灰色的點。在併發標記過程中,應用線程還在跑,因此會導致有些對象會從新生代晉升到老年代、有些老年代的對象引用會被改變、有些對象會直接分配到老年代,這些受到影響的老年代對象所在的card會被標記為dirty,用於重新標記階段掃描。這個階段過程中,老年代對象的card被標記為dirty的可能原因,就是下圖中綠色的線:

不可錯過的CMS學習筆記

3、預清理:預清理,也是用於標記老年代存活的對象,目的是為了讓重新標記階段的STW儘可能短。這個階段的目標是在併發標記階段被應用線程影響到的老年代對象,包括:(1)老年代中card為dirty的對象;(2)倖存區(from和to)中引用的老年代對象。因此,這個階段也需要掃描新生代+老年代。【PS:會不會掃描Eden區的對象,我看源代碼猜測是沒有,還需要繼續求證】

不可錯過的CMS學習筆記

4、可中斷的預清理:這個階段的目標跟“預清理”階段相同,也是為了減輕重新標記階段的工作量。可中斷預清理的價值:在進入重新標記階段之前儘量等到一個Minor GC,儘量縮短重新標記階段的停頓時間。另外可中斷預清理會在Eden達到50%的時候開始,這時候離下一次minor gc還有半程的時間,這個還有另一個意義,即避免短時間內連著的兩個停頓,如下圖資料所示:

不可錯過的CMS學習筆記

5、在預清理步驟後,如果滿足下面兩個條件,就不會開啟可中斷的預清理,直接進入重新標記階段:

  • Eden的使用空間大於“CMSScheduleRemarkEdenSizeThreshold”,這個參數的默認值是2M;
  • Eden的使用率大於等於“CMSScheduleRemarkEdenPenetration”,這個參數的默認值是50%。
  1. 如果不滿足上面兩個條件,則進入可中斷的預清理,可中斷預清理可能會執行多次,那麼退出這個階段的出口有兩個(源碼參見下圖):
  • 設置了CMSMaxAbortablePrecleanLoops,並且執行的次數超過了這個值,這個參數的默認值是0;
  • CMSMaxAbortablePrecleanTime,執行可中斷預清理的時間超過了這個值,這個參數的默認值是5000毫秒。
不可錯過的CMS學習筆記

  • 如果是因為這個原因退出,gc日誌打印如下:
不可錯過的CMS學習筆記

有可能可中斷預清理過程中一直沒等到Minor gc,這時候進入重新標記階段的話,新生代還有很多活著的對象,就回導致STW變長,因此CMS還提供了CMSScavengeBeforeRemark參數,可以在進入重新標記之前強制進行依次Minor gc。

5、(STW)重新標記:重新掃描堆中的對象,進行可達性分析,標記活著的對象。這個階段掃描的目標是:新生代的對象 + Gc Roots + 前面被標記為dirty的card對應的老年代對象。如果預清理的工作沒做好,這一步掃描新生代的時候就會花很多時間,導致這個階段的停頓時間過長。這個過程是多線程的。

6、併發清除:用戶線程被重新激活,同時將那些未被標記為存活的對象標記為不可達;

7、併發重置:CMS內部重置回收器狀態,準備進入下一個併發回收週期。

CMS的異常情況

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

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

三、CMS的調優

  1. 針對停頓時間過長的調優
  2. 首先需要判斷是哪個階段的停頓導致的,然後再針對具體的原因進行調優。使用CMS收集器的JVM可能引發停頓的情況有:(1)Minor gc的停頓;(2)併發週期裡初始標記的停頓;(3)併發週期裡重新標記的停頓;(4)Serial-Old收集老年代的停頓;(5)Full GC的停頓。其中併發模式失敗會導致第(4)種情況,晉升失敗和永久代空間耗盡會導致第(5)種情況。
  3. 針對併發模式失敗的調優
  • 想辦法增大老年代的空間,增加整個堆的大小,或者減少年輕代的大小
  • 以更高的頻率執行後臺的回收線程,即提高CMS併發週期發生的頻率。設置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數,調低CMSInitiatingOccupancyFraction的值,但是也不能調得太低,太低了會導致過多的無效的併發週期,會導致消耗CPU時間和更多的無效的停頓。通常來講,這個過程需要幾個迭代,但是還是有一定的套路,參見《Java性能權威指南》中給出的建議,摘抄如下:
> 對特定的應用程序,該標誌的更優值可以根據 GC 日誌中 CMS 週期首次啟動失敗時的值得到。具體方法是,在垃圾回收日誌中尋找併發模式失效,找到後再反向查找 CMS 週期最近的啟動記錄,然後根據日誌來計算這時候的老年代空間佔用值,然後設置一個比該值更小的值。
  • 增多回收線程的個數
CMS默認的垃圾收集線程數是*(CPU個數 + 3)/4*,這個公式的含義是:當CPU個數大於4個的時候,垃圾回收後臺線程至少佔用25%的CPU資源。舉個例子:如果CPU核數是1-4個,那麼會有1個CPU用於垃圾收集,如果CPU核數是5-8個,那麼久會有2個CPU用於垃圾收集。
  1. 針對永久代的調優
  2. 如果永久代需要垃圾回收(或元空間擴容),就會觸發Full GC。默認情況下,CMS不會處理永久代中的垃圾,可以通過開啟CMSPermGenSweepingEnabled配置來開啟永久代中的垃圾回收,開啟後會有一組後臺線程針對永久代做收集,需要注意的是,觸發永久代進行垃圾收集的指標跟觸發老年代進行垃圾收集的指標是獨立的,老年代的閾值可以通過CMSInitiatingPermOccupancyFraction參數設置,這個參數的默認值是80%。開啟對永久代的垃圾收集只是其中的一步,還需要開啟另一個參數——CMSClassUnloadingEnabled,使得在垃圾收集的時候可以卸載不用的類。

四、CMS的trade-off是什麼?

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

五、幾個問題的解答

  1. 為什麼ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?
  2. 答:這個跟Hotspot VM的歷史有關,Parallel Scanvenge是不在“分代框架”下開發的,而ParNew、CMS都是在分代框架下開發的。
  3. CMS中minor gc和major gc是順序發生的嗎?
  4. 答:不是的,可以交叉發生,即在併發週期執行過程中,是可以發生Minor gc的,這個找個gc日誌就可以觀察到。
  5. CMS的併發收集週期合適觸發?
  6. 由下圖可以看出,CMS 併發週期觸發的條件有兩個:
不可錯過的CMS學習筆記


  • 閾值檢查機制:老年代的使用空間達到某個閾值,JVM的默認值是92%(jdk1.5之前是68%,jdk1.6之後是92%),或者可以通過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個參數來設置;這個參數的設置需要看應用場景,設置得太小,會導致CMS頻繁發生,設置得太大,會導致過多的併發模式失敗。例如
  • 動態檢查機制:JVM會根據最近的回收歷史,估算下一次老年代被耗盡的時間,快到這個時間的時候就啟動一個併發週期。設置UseCMSInitiatingOccupancyOnly這個參數可以將這個特性關閉。
  1. CMS的併發收集週期會掃描哪些對象?會回收哪些對象?
  2. 答:CMS的併發週期只會回收老年代的對象,但是在標記老年代的存活對象時,可能有些對象會被年輕代的對象引用,因此需要掃描整個堆的對象。
  3. CMS的gc roots包括哪些對象?
  4. 答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(參見R大對GC roots的概念的解釋);第二,CMS的併發收集週期中,如何判斷老年代的對象是活著?我們前面提到了,在CMS的併發週期中,僅僅掃描Gc Roots直達的對象會有遺漏,還需要掃描新生代的對象。如下圖中的藍色字體所示,CMS中的年輕代和老年代是分別收集的,因此在判斷年輕代的對象存活的時候,需要把老年代當作自己的GcRoots,這時候並不需要掃描老年代的全部對象,而是使用了card table數據結構,如果一個老年代對象引用了年輕代的對象,則card中的值會被設置為特殊的數值;反過來判斷老年代對象存活的時候,也需要把年輕代當作自己的Gc Roots,這個過程我們在第三節已經論述過了。
不可錯過的CMS學習筆記


  1. 如果我的應用決定使用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
  1. 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優化

https://tech.meituan.com/jvm_optimize.html

2、理解CMS垃圾回收日誌

http://ifeve.com/jvm-cms-log/

3、圖解CMS垃圾回收機制,你值得擁有

https://www.jianshu.com/p/2a1b2f17d3e4

4、為什麼CMS雖然是老年代的gc,但仍要掃描新生代的?

​https://www.zhihu.com/question/279580656/answer/408089811

5、R大對GC roots的概念的解釋

https://www.zhihu.com/question/53613423/answer/135743258

6、Introduce to CMS Collector

https://medium.com/@robiplus/introduce-to-cms-collector-47b4400665c3

7、《深入理解Java虛擬機》

8、《Java性能權威指南》

9、Oracle的GC調優手冊

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

10、what-is-the-threshold-for-cms-old-gc-to-be-triggered

https://stackoverflow.com/questions/33557644/what-is-the-threshold-for-cms-old-gc-to-be-triggered

11、Frequently Asked Questions about Garbage Collection in the Hotspot Java VirtualMachine

https://www.oracle.com/technetwork/java/faq-140837.html

12、Java SE HotSpot at a Glance

https://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html

13、xxfox:PerfMa的參數調優神器

http://xxfox.perfma.com/

14、詳解CMS垃圾回收機制

https://www.cnblogs.com/littleLord/p/5380624.html

15、ParNew和PSYoungGen和DefNew是一個東西麼?

http://hllvm.group.iteye.com/group/topic/37095

16、Java SE的內存管理白皮書

https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf

17、Garbage Collection in Elasticsearch and the G1GC

https://medium.com/naukri-engineering/garbage-collection-in-elasticsearch-and-the-g1gc-16b79a447181

18、A Heap of Trouble

https://www.elastic.co/cn/blog/a-heap-of-trouble

19、畢玄的文章:為什麼不建議

http://hellojava.info/

20、JVM源碼分析之SystemGC完全解讀

http://lovestblog.cn/blog/2015/05/07/system-gc/

讀者討論

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


不可錯過的CMS學習筆記


  1. concurrent mode failure和promotion failed觸發的Full GC有啥不同?(這個問題是我、阿飛、蔣曉峰一起討論的結果)
  2. 答: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)我們設置了UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction,前者設置為true,後者默認是0,前者表示是在Full GC的時候執行壓縮,後者表示是每隔多少個進行壓縮,默認是0的話就是每次Full GC都壓縮;(2)用戶調用了System.gc(),而且DisableExplicitGC沒有開啟;(3)young gen報告接下來如果做增量收集會失敗。
不可錯過的CMS學習筆記


  • promotion failed觸發的是我們常說的的Full GC,對年輕代和老年代都會回收,並進行整理。
  1. promotion failed和concurrent mode failure的觸發原因有啥不同?
  • promotion failed是說,擔保機制確定老年代是否有足夠的空間容納新來的對象,如果擔保機制說有,但是真正分配的時候發現由於碎片導致找不到連續的空間而失敗;
  • concurrent mode failure是指併發週期還沒執行完,用戶線程就來請求比預留空間更大的空間了,即後臺線程的收集沒有趕上應用線程的分配速度。
  1. 什麼情況下才選擇使用CMS收集器呢?我之前的觀念是:小於8G的都用CMS,大於8G的選擇G1。蔣曉峰跟我討論了下這個觀念,提出了一些別的想法,我覺得也有道理,記錄在這裡:
  • 除了看吞吐量和延時,還需要看具體的應用,比方說ES,Lucene和G1是不兼容的,因此默認的收集器就是CMS,具體見可參考資料17和18。
  • 小於3G的堆,如果不是對延遲有特別高的需求,不建議使用CMS,主要是由於CMS的幾個缺點導致的:(1)併發週期的觸發比例不好設置;(2)搶佔CPU時間;(3)擔保判斷導致YGC變慢;(4)碎片問題,更詳細的討論參見資料19。


分享到:


相關文章: