引子
帶著問題去學習一個東西,才會有目標感,我先把一直以來自己對CMS的一些疑惑羅列了下,希望這篇學習筆記能解決掉這些疑惑,希望也能對你有所幫助。
-
CMS出現的初衷、背景和目的?
-
CMS的適用場景?
-
CMS的trade-off是什么?優勢、劣勢和代價
-
CMS會回收哪個區域的對象?
-
CMS的GC Roots包括那些對象?
-
CMS的過程?
-
CMS和Full gc是不是一回事?
-
CMS何時觸發?
-
CMS的日志如何分析?
-
CMS的調優如何做?
-
CMS掃描那些對象?
-
CMS和CMS collector的區別?
-
CMS的推薦參數設置?
-
為什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?
一、基礎知識
-
CMS收集器:Mostly-Concurrent收集器,也稱并發標記清除收集器(Concurrent Mark-Sweep GC,CMS收集器),它管理新生代的方式與Parallel收集器和Serial收集器相同,而在老年代則是盡可能得并發執行,每個垃圾收集器周期只有2次短停頓。
-
我之前對CMS的理解,以為它是針對老年代的收集器。今天查閱了《Java性能優化權威指南》和《Java性能權威指南》兩本書,確認之前的理解是錯誤的。
-
CMS的初衷和目的:為了消除Throught收集器和Serial收集器在Full GC周期中的長時間停頓。
-
CMS的適用場景:如果你的應用需要更快的響應,不希望有長時間的停頓,同時你的CPU資源也比較豐富,就適合適用CMS收集器。
二、CMS的過程
CMS的正常過程
這里我們首先看下CMS并發收集周期正常完成的幾個狀態。
1.(STW)初始標記:這個階段是標記從GcRoots直接可達的老年代對象、新生代引用的老年代對象,就是下圖中灰色的點。這個過程是單線程的。

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

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

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

如果不滿足上面兩個條件,則進入可中斷的預清理,可中斷預清理可能會執行多次,那么退出這個階段的出口有兩個(源碼參見下圖):
-
設置了CMSMaxAbortablePrecleanLoops,并且執行的次數超過了這個值,這個參數的默認值是0;
-
CMSMaxAbortablePrecleanTime,執行可中斷預清理的時間超過了這個值,這個參數的默認值是5000毫秒。
如果是因為這個原因退出,gc日志打印如下:有可能可中斷預清理過程中一直沒等到Minor gc,這時候進入重新標記階段的話,新生代還有很多活著的對象,就回導致STW變長,因此CMS還提供了CMSScavengeBeforeRemark參數,可以在進入重新標記之前強制進行依次Minor gc。
-
Eden的使用空間大于“CMSScheduleRemarkEdenSizeThreshold”,這個參數的默認值是2M;
-
Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,這個參數的默認值是50%。
5.(STW)重新標記:重新掃描堆中的對象,進行可達性分析,標記活著的對象。這個階段掃描的目標是:新生代的對象 + Gc Roots + 前面被標記為dirty的card對應的老年代對象。如果預清理的工作沒做好,這一步掃描新生代的時候就會花很多時間,導致這個階段的停頓時間過長。這個過程是多線程的。
6. 并發清除:用戶線程被重新激活,同時將那些未被標記為存活的對象標記為不可達;
7. 并發重置:CMS內部重置回收器狀態,準備進入下一個并發回收周期。
CMS的異常情況
上面描述的是CMS的并發周期正常完成的情況,但是還有幾種CMS并發周期失敗的情況:
-
并發模式失敗(Concurrent mode failure):CMS的目標就是在回收老年代對象的時候不要停止全部應用線程,在并發周期執行期間,用戶的線程依然在運行,如果這時候如果應用線程向老年代請求分配的空間超過預留的空間(擔保失敗),就回觸發concurrent mode failure,然后CMS的并發周期就會被一次Full GC代替——停止全部應用進行垃圾收集,并進行空間壓縮。如果我們設置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數,其中CMSInitiatingOccupancyFraction的值是70,那預留空間就是老年代的30%。
-
晉升失敗:新生代做minor gc的時候,需要CMS的擔保機制確認老年代是否有足夠的空間容納要晉升的對象,擔保機制發現不夠,則報concurrent mode failure,如果擔保機制判斷是夠的,但是實際上由于碎片問題導致無法分配,就會報晉升失敗。
-
永久代空間(或Java8的元空間)耗盡,默認情況下,CMS不會對永久代進行收集,一旦永久代空間耗盡,就回觸發Full GC。
三、CMS的調優
1. 針對停頓時間過長的調優 首先需要判斷是哪個階段的停頓導致的,然后再針對具體的原因進行調優。使用CMS收集器的JVM可能引發停頓的情況有:(1)Minor gc的停頓;(2)并發周期里初始標記的停頓;(3)并發周期里重新標記的停頓;(4)Serial-Old收集老年代的停頓;(5)Full GC的停頓。其中并發模式失敗會導致第(4)種情況,晉升失敗和永久代空間耗盡會導致第(5)種情況。
2. 針對并發模式失敗的調優
-
想辦法增大老年代的空間,增加整個堆的大小,或者減少年輕代的大小
-
以更高的頻率執行后臺的回收線程,即提高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用于垃圾收集。
-
針對永久代的調優 如果永久代需要垃圾回收(或元空間擴容),就會觸發Full GC。默認情況下,CMS不會處理永久代中的垃圾,可以通過開啟CMSPermGenSweepingEnabled配置來開啟永久代中的垃圾回收,開啟后會有一組后臺線程針對永久代做收集,需要注意的是,觸發永久代進行垃圾收集的指標跟觸發老年代進行垃圾收集的指標是獨立的,老年代的閾值可以通過CMSInitiatingPermOccupancyFraction參數設置,這個參數的默認值是80%。開啟對永久代的垃圾收集只是其中的一步,還需要開啟另一個參數——CMSClassUnloadingEnabled,使得在垃圾收集的時候可以卸載不用的類。
四、CMS的trade-off是什么?
-
優勢
-
低延遲的收集器:幾乎沒有長時間的停頓,應用程序只在Minor gc以及后臺線程掃描老年代的時候發生極其短暫的停頓。
劣勢
-
更高的CPU使用:必須有足夠的CPU資源用于運行后臺的垃圾收集線程,在應用程序線程運行的同時掃描堆的使用情況。【PS:現在服務器的CPU資源基本不是問題,這個點可以忽略】
-
CMS收集器對老年代收集的時候,不再進行任何壓縮和整理的工作,意味著老年代隨著應用的運行會變得碎片化;碎片過多會影響大對象的分配,雖然老年代還有很大的剩余空間,但是沒有連續的空間來分配大對象,這時候就會觸發Full GC。CMS提供了兩個參數來解決這個問題:(1)UseCMSCompactAtFullCollection,在要進行Full GC的時候進行內存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不壓縮的Full GC后,執行一次帶壓縮的Full GC。
-
會出現浮動垃圾;在并發清理階段,用戶線程仍然在運行,必須預留出空間給用戶線程使用,因此CMS比其他回收器需要更大的堆空間。
五、幾個問題的解答
-
為什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以? 答:這個跟Hotspot VM的歷史有關,Parallel Scanvenge是不在“分代框架”下開發的,而ParNew、CMS都是在分代框架下開發的。
-
CMS中minor gc和major gc是順序發生的嗎? 答:不是的,可以交叉發生,即在并發周期執行過程中,是可以發生Minor gc的,這個找個gc日志就可以觀察到。
-
CMS的并發收集周期合適觸發? 由下圖可以看出,CMS 并發周期觸發的條件有兩個:
-
閾值檢查機制:老年代的使用空間達到某個閾值,JVM的默認值是92%(jdk1.5之前是68%,jdk1.6之后是92%),或者可以通過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個參數來設置;這個參數的設置需要看應用場景,設置得太小,會導致CMS頻繁發生,設置得太大,會導致過多的并發模式失敗。例如
-
動態檢查機制:JVM會根據最近的回收歷史,估算下一次老年代被耗盡的時間,快到這個時間的時候就啟動一個并發周期。設置UseCMSInitiatingOccupancyOnly這個參數可以將這個特性關閉。
CMS的并發收集周期會掃描哪些對象?會回收哪些對象? 答:CMS的并發周期只會回收老年代的對象,但是在標記老年代的存活對象時,可能有些對象會被年輕代的對象引用,因此需要掃描整個堆的對象。
CMS的gc roots包括哪些對象? 答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(參見R大對GC roots的概念的解釋);第二,CMS的并發收集周期中,如何判斷老年代的對象是活著?我們前面提到了,在CMS的并發周期中,僅僅掃描Gc Roots直達的對象會有遺漏,還需要掃描新生代的對象。如下圖中的藍色字體所示,CMS中的年輕代和老年代是分別收集的,因此在判斷年輕代的對象存活的時候,需要把老年代當作自己的GcRoots,這時候并不需要掃描老年代的全部對象,而是使用了card table數據結構,如果一個老年代對象引用了年輕代的對象,則card中的值會被設置為特殊的數值;反過來判斷老年代對象存活的時候,也需要把年輕代當作自己的Gc Roots,這個過程我們在第三節已經論述過了。

-
-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
CMS相關的參數總結(需要注意的是,這里我沒有考慮太多JDK版本的問題,JDK1.7和JDK1.8這些參數的配置,有些默認值可能不一樣,具體使用的時候還需要根據具體的版本來確認怎么設置)

參考資料
-
從實際案例聊聊Java應用的GC優化
-
理解CMS垃圾回收日志
-
圖解CMS垃圾回收機制,你值得擁有
-
為什么CMS雖然是老年代的gc,但仍要掃描新生代的?
-
R大對GC roots的概念的解釋
-
Introduce to CMS Collector
-
《深入理解Java虛擬機》
-
《Java性能權威指南》
-
Oracle的GC調優手冊
-
what-is-the-threshold-for-cms-old-gc-to-be-triggered
-
Frequently Asked Questions about Garbage Collection in the Hotspot Java VirtualMachine
-
Java SE HotSpot at a Glance
-
xxfox:PerfMa的參數調優神器
-
詳解CMS垃圾回收機制
-
ParNew和PSYoungGen和DefNew是一個東西么?
-
Java SE的內存管理白皮書
本文作者:阿杜的世界
本文來自云棲社區合作伙伴“ Kirito的技術分享”,了解相關信息可以關注“Kirito的技術分享”。