深入浅出 Java CMS 学习笔记
轉(zhuǎn)載自??深入淺出 Java CMS 學(xué)習(xí)筆記
?
引子
帶著問題去學(xué)習(xí)一個東西,才會有目標(biāo)感,我先把一直以來自己對CMS的一些疑惑羅列了下,希望這篇學(xué)習(xí)筆記能解決掉這些疑惑,希望也能對你有所幫助。
1、 CMS出現(xiàn)的初衷、背景和目的?
2、 CMS的適用場景?
3、 CMS的trade-off是什么?優(yōu)勢、劣勢和代價
4、 CMS會回收哪個區(qū)域的對象?
5、 CMS的GC Roots包括那些對象?
6、 CMS的過程?
7、 CMS和Full gc是不是一回事?
8、 CMS何時觸發(fā)?
9、 CMS的日志如何分析?
10、 CMS的調(diào)優(yōu)如何做?
11、 CMS掃描那些對象?
12、 CMS和CMS collector的區(qū)別?
13、 CMS的推薦參數(shù)設(shè)置?
14、 為什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?
一、基礎(chǔ)知識
1、 CMS收集器:Mostly-Concurrent收集器,也稱并發(fā)標(biāo)記清除收集器(Concurrent Mark-Sweep GC,CMS收集器),它管理新生代的方式與Parallel收集器和Serial收集器相同,而在老年代則是盡可能得并發(fā)執(zhí)行,每個垃圾收集器周期只有2次短停頓。
2、 我之前對CMS的理解,以為它是針對老年代的收集器。今天查閱了《Java性能優(yōu)化權(quán)威指南》和《Java性能權(quán)威指南》兩本書,確認(rèn)之前的理解是錯誤的。
3、 CMS的初衷和目的:為了消除Throught收集器和Serial收集器在Full GC周期中的長時間停頓。
4、 CMS的適用場景:如果你的應(yīng)用需要更快的響應(yīng),不希望有長時間的停頓,同時你的CPU資源也比較豐富,就適合適用CMS收集器。
二、CMS的過程
CMS的正常過程
這里我們首先看下CMS并發(fā)收集周期正常完成的幾個狀態(tài)。
1、(STW)初始標(biāo)記:這個階段是標(biāo)記從GcRoots直接可達的老年代對象、新生代引用的老年代對象,就是下圖中灰色的點。這個過程是單線程的(JDK7之前單線程,JDK8之后并行,可以通過參數(shù)CMSParallelInitialMarkEnabled調(diào)整)。
?
初始標(biāo)記標(biāo)記的對象
2、并發(fā)標(biāo)記:由上一個階段標(biāo)記過的對象,開始tracing過程,標(biāo)記所有可達的對象,這個階段垃圾回收線程和應(yīng)用線程同時運行,如上圖中的灰色的點。在并發(fā)標(biāo)記過程中,應(yīng)用線程還在跑,因此會導(dǎo)致有些對象會從新生代晉升到老年代、有些老年代的對象引用會被改變、有些對象會直接分配到老年代,這些受到影響的老年代對象所在的card會被標(biāo)記為dirty,用于重新標(biāo)記階段掃描。這個階段過程中,老年代對象的card被標(biāo)記為dirty的可能原因,就是下圖中綠色的線:
?
并發(fā)標(biāo)記過程中受到影響的對象
3、預(yù)清理:預(yù)清理,也是用于標(biāo)記老年代存活的對象,目的是為了讓重新標(biāo)記階段的STW盡可能短。這個階段的目標(biāo)是在并發(fā)標(biāo)記階段被應(yīng)用線程影響到的老年代對象,包括:(1)老年代中card為dirty的對象;(2)幸存區(qū)(from和to)中引用的老年代對象。因此,這個階段也需要掃描新生代+老年代。【PS:會不會掃描Eden區(qū)的對象,我看源代碼猜測是沒有,還需要繼續(xù)求證】
?
預(yù)清理中掃描from和to區(qū)
4、可中斷的預(yù)清理:這個階段的目標(biāo)跟“預(yù)清理”階段相同,也是為了減輕重新標(biāo)記階段的工作量。可中斷預(yù)清理的價值:在進入重新標(biāo)記階段之前盡量等到一個Minor GC,盡量縮短重新標(biāo)記階段的停頓時間。另外可中斷預(yù)清理會在Eden達到50%的時候開始,這時候離下一次minor gc還有半程的時間,這個還有另一個意義,即避免短時間內(nèi)連著的兩個停頓,如下圖資料所示:
?
避免連續(xù)停頓的發(fā)生
在預(yù)清理步驟后,如果滿足下面兩個條件,就不會開啟可中斷的預(yù)清理,直接進入重新標(biāo)記階段:
-
Eden的使用空間大于“CMSScheduleRemarkEdenSizeThreshold”,這個參數(shù)的默認(rèn)值是2M;
-
Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,這個參數(shù)的默認(rèn)值是50%。
如果不滿足上面兩個條件,則進入可中斷的預(yù)清理,可中斷預(yù)清理可能會執(zhí)行多次,那么退出這個階段的出口有兩個(源碼參見下圖):
-
設(shè)置了CMSMaxAbortablePrecleanLoops,并且執(zhí)行的次數(shù)超過了這個值,這個參數(shù)的默認(rèn)值是0;
-
CMSMaxAbortablePrecleanTime,執(zhí)行可中斷預(yù)清理的時間超過了這個值,這個參數(shù)的默認(rèn)值是5000毫秒。
-
可中斷預(yù)清理退出的條件
如果是因為這個原因退出,gc日志打印如下:
可中斷預(yù)清理由于時間退出
有可能可中斷預(yù)清理過程中一直沒等到Minor gc,這時候進入重新標(biāo)記階段的話,新生代還有很多活著的對象,就回導(dǎo)致STW變長,因此CMS還提供了CMSScavengeBeforeRemark參數(shù),可以在進入重新標(biāo)記之前強制進行依次Minor gc。
5、 (STW)重新標(biāo)記:重新掃描堆中的對象,進行可達性分析,標(biāo)記活著的對象。這個階段掃描的目標(biāo)是:新生代的對象 + Gc Roots + 前面被標(biāo)記為dirty的card對應(yīng)的老年代對象。如果預(yù)清理的工作沒做好,這一步掃描新生代的時候就會花很多時間,導(dǎo)致這個階段的停頓時間過長。這個過程是多線程的。
6、并發(fā)清除:用戶線程被重新激活,同時將那些未被標(biāo)記為存活的對象標(biāo)記為不可達;
7、 并發(fā)重置:CMS內(nèi)部重置回收器狀態(tài),準(zhǔn)備進入下一個并發(fā)回收周期。
CMS的異常情況
上面描述的是CMS的并發(fā)周期正常完成的情況,但是還有幾種CMS并發(fā)周期失敗的情況:
1、并發(fā)模式失敗(Concurrent mode failure):CMS的目標(biāo)就是在回收老年代對象的時候不要停止全部應(yīng)用線程,在并發(fā)周期執(zhí)行期間,用戶的線程依然在運行,如果這時候如果應(yīng)用線程向老年代請求分配的空間超過預(yù)留的空間(擔(dān)保失敗),就回觸發(fā)concurrent mode failure,然后CMS的并發(fā)周期就會被一次Full GC代替——停止全部應(yīng)用進行垃圾收集,并進行空間壓縮。如果我們設(shè)置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數(shù),其中CMSInitiatingOccupancyFraction的值是70,那預(yù)留空間就是老年代的30%。
2、 晉升失敗:新生代做minor gc的時候,需要CMS的擔(dān)保機制確認(rèn)老年代是否有足夠的空間容納要晉升的對象,擔(dān)保機制發(fā)現(xiàn)不夠,則報concurrent mode failure,如果擔(dān)保機制判斷是夠的,但是實際上由于碎片問題導(dǎo)致無法分配,就會報晉升失敗。
3、 永久代空間(或Java8的元空間)耗盡,默認(rèn)情況下,CMS不會對永久代進行收集,一旦永久代空間耗盡,就回觸發(fā)Full GC。
三、CMS的調(diào)優(yōu)
1、 針對停頓時間過長的調(diào)優(yōu)
首先需要判斷是哪個階段的停頓導(dǎo)致的,然后再針對具體的原因進行調(diào)優(yōu)。使用CMS收集器的JVM可能引發(fā)停頓的情況有:
(1)Minor gc的停頓;
(2)并發(fā)周期里初始標(biāo)記的停頓;
(3)并發(fā)周期里重新標(biāo)記的停頓;
(4)Serial-Old收集老年代的停頓;
(5)Full GC的停頓。其中并發(fā)模式失敗會導(dǎo)致第(4)種情況,晉升失敗和永久代空間耗盡會導(dǎo)致第(5)種情況。
2、 針對并發(fā)模式失敗的調(diào)優(yōu)
-
想辦法增大老年代的空間,增加整個堆的大小,或者減少年輕代的大小
-
以更高的頻率執(zhí)行后臺的回收線程,即提高CMS并發(fā)周期發(fā)生的頻率。設(shè)置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數(shù),調(diào)低CMSInitiatingOccupancyFraction的值,但是也不能調(diào)得太低,太低了會導(dǎo)致過多的無效的并發(fā)周期,會導(dǎo)致消耗CPU時間和更多的無效的停頓。通常來講,這個過程需要幾個迭代,但是還是有一定的套路,參見《Java性能權(quán)威指南》中給出的建議,摘抄如下:
“ 對特定的應(yīng)用程序,該標(biāo)志的更優(yōu)值可以根據(jù) GC 日志中 CMS 周期首次啟動失敗時的值得到。具體方法是,在垃圾回收日志中尋找并發(fā)模式失效,找到后再反向查找 CMS 周期最近的啟動記錄,然后根據(jù)日志來計算這時候的老年代空間占用值,然后設(shè)置一個比該值更小的值。”
-
增多回收線程的個數(shù)
CMS默認(rèn)的垃圾收集線程數(shù)是*(CPU個數(shù)?+?3)/4*,這個公式的含義是:當(dāng)CPU個數(shù)大于4個的時候,垃圾回收后臺線程至少占用25%的CPU資源。舉個例子:如果CPU核數(shù)是1-4個,那么會有1個CPU用于垃圾收集,如果CPU核數(shù)是5-8個,那么久會有2個CPU用于垃圾收集。
3、 針對永久代的調(diào)優(yōu)
如果永久代需要垃圾回收(或元空間擴容),就會觸發(fā)Full GC。默認(rèn)情況下,CMS不會處理永久代中的垃圾,可以通過開啟CMSPermGenSweepingEnabled配置來開啟永久代中的垃圾回收,開啟后會有一組后臺線程針對永久代做收集,需要注意的是,觸發(fā)永久代進行垃圾收集的指標(biāo)跟觸發(fā)老年代進行垃圾收集的指標(biāo)是獨立的,老年代的閾值可以通過CMSInitiatingPermOccupancyFraction參數(shù)設(shè)置,這個參數(shù)的默認(rèn)值是80%。開啟對永久代的垃圾收集只是其中的一步,還需要開啟另一個參數(shù)——CMSClassUnloadingEnabled,使得在垃圾收集的時候可以卸載不用的類。
四、CMS的trade-off是什么?
1、 優(yōu)勢
-
低延遲的收集器:幾乎沒有長時間的停頓,應(yīng)用程序只在Minor gc以及后臺線程掃描老年代的時候發(fā)生極其短暫的停頓。
2、 劣勢
-
更高的CPU使用:必須有足夠的CPU資源用于運行后臺的垃圾收集線程,在應(yīng)用程序線程運行的同時掃描堆的使用情況。【PS:現(xiàn)在服務(wù)器的CPU資源基本不是問題,這個點可以忽略】
-
CMS收集器對老年代收集的時候,不再進行任何壓縮和整理的工作,意味著老年代隨著應(yīng)用的運行會變得碎片化;碎片過多會影響大對象的分配,雖然老年代還有很大的剩余空間,但是沒有連續(xù)的空間來分配大對象,這時候就會觸發(fā)Full GC。CMS提供了兩個參數(shù)來解決這個問題:(1)UseCMSCompactAtFullCollection,在要進行Full GC的時候進行內(nèi)存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不壓縮的Full GC后,執(zhí)行一次帶壓縮的Full GC。
-
會出現(xiàn)浮動垃圾;在并發(fā)清理階段,用戶線程仍然在運行,必須預(yù)留出空間給用戶線程使用,因此CMS比其他回收器需要更大的堆空間。
五、幾個問題的解答
1、為什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?
答:這個跟Hotspot VM的歷史有關(guān),Parallel Scanvenge是不在“分代框架”下開發(fā)的,而ParNew、CMS都是在分代框架下開發(fā)的。
2、CMS中minor gc和major gc是順序發(fā)生的嗎?
答:不是的,可以交叉發(fā)生,即在并發(fā)周期執(zhí)行過程中,是可以發(fā)生Minor gc的,這個找個gc日志就可以觀察到。
3、CMS的并發(fā)收集周期合適觸發(fā)?
由下圖可以看出,CMS 并發(fā)周期觸發(fā)的條件有兩個:
?
觸發(fā)cms并發(fā)周期的條件
-
閾值檢查機制:老年代的使用空間達到某個閾值,JVM的默認(rèn)值是92%(jdk1.5之前是68%,jdk1.6之后是92%),或者可以通過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個參數(shù)來設(shè)置;這個參數(shù)的設(shè)置需要看應(yīng)用場景,設(shè)置得太小,會導(dǎo)致CMS頻繁發(fā)生,設(shè)置得太大,會導(dǎo)致過多的并發(fā)模式失敗。例如
-
動態(tài)檢查機制:JVM會根據(jù)最近的回收歷史,估算下一次老年代被耗盡的時間,快到這個時間的時候就啟動一個并發(fā)周期。設(shè)置UseCMSInitiatingOccupancyOnly這個參數(shù)可以將這個特性關(guān)閉。
4、CMS的并發(fā)收集周期會掃描哪些對象?會回收哪些對象?
答:CMS的并發(fā)周期只會回收老年代的對象,但是在標(biāo)記老年代的存活對象時,可能有些對象會被年輕代的對象引用,因此需要掃描整個堆的對象。
5、CMS的gc roots包括哪些對象?
答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(參見R大對GC roots的概念的解釋);第二,CMS的并發(fā)收集周期中,如何判斷老年代的對象是活著?我們前面提到了,在CMS的并發(fā)周期中,僅僅掃描Gc Roots直達的對象會有遺漏,還需要掃描新生代的對象。如下圖中的藍色字體所示,CMS中的年輕代和老年代是分別收集的,因此在判斷年輕代的對象存活的時候,需要把老年代當(dāng)作自己的GcRoots,這時候并不需要掃描老年代的全部對象,而是使用了card table數(shù)據(jù)結(jié)構(gòu),如果一個老年代對象引用了年輕代的對象,則card中的值會被設(shè)置為特殊的數(shù)值;反過來判斷老年代對象存活的時候,也需要把年輕代當(dāng)作自己的Gc Roots,這個過程我們在第三節(jié)已經(jīng)論述過了。
?
老年代和新生代互相作為Gc Roots
6、如果我的應(yīng)用決定使用CMS收集器,推薦的JVM參數(shù)是什么?
答:我自己的應(yīng)用使用的參數(shù)如下,是根據(jù)PerfMa的xxfox生成的,大家也可以使用這個產(chǎn)品調(diào)優(yōu)自己的JVM參數(shù):
-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相關(guān)的參數(shù)總結(jié)(需要注意的是,這里我沒有考慮太多JDK版本的問題,JDK1.7和JDK1.8這些參數(shù)的配置,有些默認(rèn)值可能不一樣,具體使用的時候還需要根據(jù)具體的版本來確認(rèn)怎么設(shè)置)
?
六、讀者討論
1、關(guān)于CMS收集器的回收范圍,下面這張圖是有誤導(dǎo)的,從官方文檔上看來,CMS收集器包括年輕代和老年代的收集,只不過對年輕代的收集的策略和ParNew相同。
?
?
2、concurrent mode failure和promotion failed觸發(fā)的Full GC有啥不同?(這個問題是我、阿飛、蔣曉峰一起討論的結(jié)果)
答:concurrent mode failure觸發(fā)的"Full GC"不是我們常說的Full GC——正常的Full GC其實是整個gc過程包括ygc和cms gc。也就是說,這個問題本身是有問題的,concurrent mode failure的時候觸發(fā)的并不是我們常說的Full GC。然后再去討論一個遺漏的知識點:CMS gc的并發(fā)周期有兩種模式:foreground和background。
-
concurrent mode failure觸發(fā)的是foreground模式,會暫停整個應(yīng)用,會將一些并行的階段省掉做一次老年代收集,行為跟Serial-Old的一樣,至于在這個過程中是否需要壓縮,則需要看三個條件:
① 我們設(shè)置了UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction,前者設(shè)置為true,后者默認(rèn)是0,前者表示是在Full GC的時候執(zhí)行壓縮,后者表示是每隔多少個進行壓縮,默認(rèn)是0的話就是每次Full GC都壓縮;
② 用戶調(diào)用了System.gc(),而且DisableExplicitGC沒有開啟;
③ young gen報告接下來如果做增量收集會失敗。
-
promotion failed觸發(fā)的是我們常說的的Full GC,對年輕代和老年代都會回收,并進行整理。
3、promotion failed和concurrent mode failure的觸發(fā)原因有啥不同?
-
promotion failed是說,擔(dān)保機制確定老年代是否有足夠的空間容納新來的對象,如果擔(dān)保機制說有,但是真正分配的時候發(fā)現(xiàn)由于碎片導(dǎo)致找不到連續(xù)的空間而失敗;
-
concurrent mode failure是指并發(fā)周期還沒執(zhí)行完,用戶線程就來請求比預(yù)留空間更大的空間了,即后臺線程的收集沒有趕上應(yīng)用線程的分配速度。
4、什么情況下才選擇使用CMS收集器呢?
答:我之前的觀念是:小于8G的都用CMS,大于8G的選擇G1。蔣曉峰跟我討論了下這個觀念,提出了一些別的想法,我覺得也有道理,記錄在這里:
-
除了看吞吐量和延時,還需要看具體的應(yīng)用,比方說ES,Lucene和G1是不兼容的,因此默認(rèn)的收集器就是CMS。
-
小于3G的堆,如果不是對延遲有特別高的需求,不建議使用CMS,主要是由于CMS的幾個缺點導(dǎo)致的:(1)并發(fā)周期的觸發(fā)比例不好設(shè)置;(2)搶占CPU時間;(3)擔(dān)保判斷導(dǎo)致YGC變慢;(4)碎片問題。
?
-END-
總結(jié)
以上是生活随笔為你收集整理的深入浅出 Java CMS 学习笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网上银行密码忘了怎么办 修改方法如下
- 下一篇: JavaFX鼠标拖拽事件