不可錯過的CMS學習筆記
引子
帶著問題去學習一個東西,才會有目標感,我先把一直以來自己對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併發收集週期正常完成的幾個狀態。
- (STW)初始標記:這個階段是標記從GcRoots直接可達的老年代物件、新生代引用的老年代物件,就是下圖中灰色的點。這個過程是單執行緒的。
初始標記標記的物件
- 併發標記:由上一個階段標記過的物件,開始tracing過程,標記所有可達的物件,這個階段垃圾回收執行緒和應用執行緒同時執行,如上圖中的黃色的點。在併發標記過程中,應用執行緒還在跑,因此會導致有些物件會從新生代晉升到老年代、有些老年代的物件引用會被改變、有些物件會直接分配到老年代,這些受到影響的老年代物件所在的card會被標記為dirty,用於重新標記階段掃描。這個階段過程中,老年代物件的card被標記為dirty的可能原因,就是下圖中綠色的線:
併發標記過程中受到影響的物件
- 預清理:預清理,也是用於標記老年代存活的物件,目的是為了讓重新標記階段的STW儘可能短。這個階段的目標是在併發標記階段被應用執行緒影響到的老年代物件,包括:(1)老年代中card為dirty的物件;(2)倖存區(from和to)中引用的老年代物件。因此,這個階段也需要掃描新生代+老年代。【PS:會不會掃描Eden區的物件,我看原始碼猜測是沒有,還需要繼續求證】
預清理中掃描from和to區
-
可中斷的預清理:這個階段的目標跟“預清理”階段相同,也是為了減輕重新標記階段的工作量。可中斷預清理的價值:在進入重新標記階段之前儘量等到一個Minor GC,儘量縮短重新標記階段的停頓時間。另外可中斷預清理會在Eden達到50%的時候開始,這時候離下一次minor gc還有半程的時間,這個還有另一個意義,即避免短時間內連著的兩個停頓,如下圖資料所示:
避免連續停頓的發生
在預清理步驟後,如果滿足下面兩個條件,就不會開啟可中斷的預清理,直接進入重新標記階段:
- Eden的使用空間大於“CMSScheduleRemarkEdenSizeThreshold”,這個引數的預設值是2M;
- Eden的使用率大於等於“CMSScheduleRemarkEdenPenetration”,這個引數的預設值是50%。
如果不滿足上面兩個條件,則進入可中斷的預清理,可中斷預清理可能會執行多次,那麼退出這個階段的出口有兩個(原始碼參見下圖):
-
設定了CMSMaxAbortablePrecleanLoops,並且執行的次數超過了這個值,這個引數的預設值是0;
- CMSMaxAbortablePrecleanTime,執行可中斷預清理的時間超過了這個值,這個引數的預設值是5000毫秒。
可中斷預清理退出的條件
可中斷預清理由於時間退出
有可能可中斷預清理過程中一直沒等到Minor gc,這時候進入重新標記階段的話,新生代還有很多活著的物件,就回導致STW變長,因此CMS還提供了 CMSScavengeBeforeRemark 引數,可以在進入重新標記之前強制進行依次Minor gc。
-
(STW)重新標記:重新掃描堆中的物件,進行可達性分析,標記活著的物件。這個階段掃描的目標是:新生代的物件 + Gc Roots + 前面被標記為dirty的card對應的老年代物件。如果預清理的工作沒做好,這一步掃描新生代的時候就會花很多時間,導致這個階段的停頓時間過長。這個過程是多執行緒的。
-
併發清除:使用者執行緒被重新啟用,同時將那些未被標記為存活的物件標記為不可達;
-
併發重置: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的調優
- 針對停頓時間過長的調優
首先需要判斷是哪個階段的停頓導致的,然後再針對具體的原因進行調優。使用CMS收集器的JVM可能引發停頓的情況有:(1)Minor gc的停頓;(2)併發週期裡初始標記的停頓;(3)併發週期裡重新標記的停頓;(4)Serial-Old收集老年代的停頓;(5)Full GC的停頓。其中併發模式失敗會導致第(4)種情況,晉升失敗和永久代空間耗盡會導致第(5)種情況。 - 針對併發模式失敗的調優
-
想辦法增大老年代的空間,增加整個堆的大小,或者減少年輕代的大小
-
以更高的頻率執行後臺的回收執行緒,即提高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 併發週期觸發的條件有兩個:
觸發cms併發週期的條件
- 閾值檢查機制:老年代的使用空間達到某個閾值,JVM的預設值是92%(jdk1.5之前是68%,jdk1.6之後是92%),或者可以通過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個引數來設定;這個引數的設定需要看應用場景,設定得太小,會導致CMS頻繁發生,設定得太大,會導致過多的併發模式失敗。例如
- 動態檢查機制:JVM會根據最近的回收歷史,估算下一次老年代被耗盡的時間,快到這個時間的時候就啟動一個併發週期。設定 UseCMSInitiatingOccupancyOnly 這個引數可以將這個特性關閉。
-
CMS的併發收集週期會掃描哪些物件?會回收哪些物件?
答:CMS的併發週期只會回收老年代的物件,但是在標記老年代的存活物件時,可能有些物件會被年輕代的物件引用,因此需要掃描整個堆的物件。
-
CMS的gc roots包括哪些物件?
答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(參見 ofollow,noindex">R大對GC roots的概念的解釋 );第二,CMS的併發收集週期中,如何判斷老年代的物件是活著?我們前面提到了,在CMS的併發週期中,僅僅掃描Gc Roots直達的物件會有遺漏,還需要掃描新生代的物件。如下圖中的藍色字型所示,CMS中的年輕代和老年代是分別收集的,因此在判斷年輕代的物件存活的時候,需要把老年代當作自己的GcRoots,這時候並不需要掃描老年代的全部物件,而是使用了card table資料結構,如果一個老年代物件引用了年輕代的物件,則card中的值會被設定為特殊的數值;反過來判斷老年代物件存活的時候,也需要把年輕代當作自己的Gc Roots,這個過程我們在第三節已經論述過了。
老年代和新生代互相作為Gc Roots
-
如果我的應用決定使用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
-
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
參考資料
- 從實際案例聊聊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的記憶體管理白皮書