引子帶著問題去學(xué)習(xí)一個(gè)東西,才會(huì)有目標(biāo)感,我先把一直以來自己對(duì)CMS的有的疑惑羅列了下,渴望這一篇學(xué)習(xí)筆記能解決掉這些疑惑,渴望也能對(duì)你有所幫助。
CMS出現(xiàn)的初心、背景和目的?CMS的適合用場(chǎng)景?CMS的trade-off是什么?優(yōu)勢(shì)、劣勢(shì)和代價(jià)CMS會(huì)回收哪個(gè)地區(qū)的對(duì)象?CMS的GC Roots包括那些對(duì)象?CMS的過程?CMS和Full gc是不是一回事?CMS何時(shí)觸發(fā)?CMS的日志怎么樣解析?CMS的調(diào)優(yōu)怎么樣做?CMS掃描那些對(duì)象?CMS和CMS collector的區(qū)別?CMS的介紹參數(shù)設(shè)置?為什么ParNew可以和CMS搭配使用,而Parallel Scanvenge不可以?一、基本知識(shí)CMS獲得器:Mostly-Concurrent獲得器,也稱并發(fā)標(biāo)記清除獲得器(Concurrent Mark-Sweep GC,CMS獲得器),它管理新生代的方法與Parallel獲得器和Serial獲得器相同,而在老時(shí)代則是盡可能得并發(fā)執(zhí)行,每一個(gè)垃圾獲得器周期只有2次短停頓。我以前對(duì)CMS的理解,以為它是針對(duì)老時(shí)代的獲得器。今天查閱了《Java性能優(yōu)化權(quán)威指南》和《Java性能權(quán)威指南》兩本書,確認(rèn)以前的理解是錯(cuò)誤的。CMS的初心和目的:為了消除Throught獲得器和Serial獲得器在Full GC周期中的很長(zhǎng)時(shí)間停頓。CMS的適合用場(chǎng)景:如果你的應(yīng)用需要更快的響應(yīng),不渴望有很長(zhǎng)時(shí)間的停頓,同一時(shí)間你的CPU資源也比較多姿多彩,就適適合用CMS獲得器。二、CMS的過程CMS的正常過程
這里我們首先就這樣看下CMS并發(fā)獲得周期正常完成的幾個(gè)情況。
(STW)初始標(biāo)記:這種階段是標(biāo)記從GcRoots直接可達(dá)的老時(shí)代對(duì)象、新生代引用的老時(shí)代對(duì)象,就是下圖中灰色的點(diǎn)。這種過程是單線程的(JDK7以前單線程,JDK8之后并行,可以通過參數(shù)CMSParallelInitialMarkEnabled修改)。
初始標(biāo)記標(biāo)記的對(duì)象
并發(fā)標(biāo)記:由上一個(gè)階段標(biāo)記過的對(duì)象,開始tracing過程,標(biāo)記任何可達(dá)的對(duì)象,這種階段垃圾回收線程和應(yīng)用線程同一時(shí)間運(yùn)行,如上圖中的灰色的點(diǎn)。在并發(fā)標(biāo)記過程中,應(yīng)用線程還在跑,因此會(huì)導(dǎo)致有的對(duì)象會(huì)從新生代晉升到老時(shí)代、有的老時(shí)代的對(duì)象引用會(huì)被變化、有的對(duì)象會(huì)直接分配到老時(shí)代,這些受到波及的老時(shí)代對(duì)象所在的card會(huì)被標(biāo)記為dirty,用來從頭開始標(biāo)記階段掃描。這種階段過程中,老時(shí)代對(duì)象的card被標(biāo)記為dirty的可能原因,就是下圖中綠帽色的線:
并發(fā)標(biāo)記過程中受到波及的對(duì)象
預(yù)清理:預(yù)清理,也是用來標(biāo)記老時(shí)代存活的對(duì)象,目的是為了讓從頭開始標(biāo)記階段的STW盡可能短。這種階段的目標(biāo)是在并發(fā)標(biāo)記階段被應(yīng)用線程波及到的老時(shí)代對(duì)象,包括:(1)老時(shí)代中card為dirty的對(duì)象;(2)幸存區(qū)(from和to)中引用的老時(shí)代對(duì)象。因此,這種階段也需要掃描新生代+老時(shí)代。【PS:會(huì)不會(huì)掃描Eden區(qū)的對(duì)象,我就這樣看源代碼猜測(cè)是沒有,還需要繼續(xù)求證】
預(yù)清理中掃描from和to區(qū)
可中斷的預(yù)清理:這種階段的目標(biāo)跟“預(yù)清理”階段相同,也是為了減少?gòu)念^開始標(biāo)記階段的事情量。可中斷預(yù)清理的價(jià)值:在進(jìn)入從頭開始標(biāo)記階段以前盡量等到一個(gè)Minor GC,盡量縮短從頭開始標(biāo)記階段的停頓時(shí)光。另外可中斷預(yù)清理會(huì)在Eden達(dá)到50%的時(shí)候開始,這時(shí)候離下一次minor gc還有半程的時(shí)光,這種還有另外一個(gè)意義,即避免短暫的時(shí)間內(nèi)連著的兩個(gè)停頓,如下圖資料所示:
避免連續(xù)停頓的發(fā)生
在預(yù)清理步驟后,如果滿足下面兩個(gè)條件,就不會(huì)開啟可中斷的預(yù)清理,直接進(jìn)入從頭開始標(biāo)記階段:
Eden的使用空間大于“CMSScheduleRemarkEdenSizeThreshold”,這種參數(shù)的默認(rèn)值是2M;Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,這種參數(shù)的默認(rèn)值是50%。如果不滿足上面兩個(gè)條件,則進(jìn)入可中斷的預(yù)清理,可中斷預(yù)清理可能會(huì)執(zhí)行多次,那么退出這種階段的出口有兩個(gè)(源碼參見下圖):
設(shè)置了CMSMaxAbortablePrecleanLoops,并且執(zhí)行的次數(shù)超過了這種值,這種參數(shù)的默認(rèn)值是0; CMSMaxAbortablePrecleanTime,執(zhí)行可中斷預(yù)清理的時(shí)光超過了這種值,這種參數(shù)的默認(rèn)值是5000毫秒。
可中斷預(yù)清理退出的條件
如果是因?yàn)檫@種原因退出,gc日志打印如下:
可中斷預(yù)清理由于時(shí)光退出
有可能可中斷預(yù)清理過程中一直沒等到Minor gc,這時(shí)候進(jìn)入從頭開始標(biāo)記階段的話,新生代還有非常多活著的對(duì)象,就回導(dǎo)致STW變長(zhǎng),因此CMS還提供了CMSScavengeBeforeRemark參數(shù),可以在進(jìn)入從頭開始標(biāo)記以前強(qiáng)烈進(jìn)行依次Minor gc。
(STW)從頭開始標(biāo)記:從頭開始掃描堆中的對(duì)象,進(jìn)行可達(dá)性解析,標(biāo)記活著的對(duì)象。這種階段掃描的目標(biāo)是:新生代的對(duì)象 + Gc Roots + 前面被標(biāo)記為dirty的card對(duì)應(yīng)的老時(shí)代對(duì)象。如果預(yù)清理的事情沒做好,這一步掃描新生代的時(shí)候就會(huì)花非常多的時(shí)間,導(dǎo)致這種階段的停頓時(shí)光過長(zhǎng)。這種過程是多線程的。并發(fā)清除:玩家線程被從頭開始激活,同一時(shí)間將那些未被標(biāo)記為存活的對(duì)象標(biāo)記為不可達(dá);并發(fā)重置:CMS內(nèi)部重置回收器情況,準(zhǔn)備進(jìn)入下一個(gè)并發(fā)回收周期。CMS的不正常狀態(tài)上面描述的是CMS的并發(fā)周期正常完成的狀態(tài),但是還有幾種CMS并發(fā)周期失敗的狀態(tài):
并發(fā)模式失敗(Concurrent mode failure):CMS的目標(biāo)就是在回收老時(shí)代對(duì)象的時(shí)候不要終止全部應(yīng)用線程,在并發(fā)周期執(zhí)行期間,玩家的線程依然在運(yùn)行,如果這時(shí)候如果應(yīng)用線程向老時(shí)代請(qǐng)求分配的空間超過預(yù)留的空間(擔(dān)保失敗),就回觸發(fā)concurrent mode failure,之后跟著CMS的并發(fā)周期就會(huì)被一次Full GC代替——終止全部應(yīng)用進(jìn)行垃圾獲得,并進(jìn)行空間壓縮。如果我們?cè)O(shè)置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數(shù),之中CMSInitiatingOccupancyFraction的值是70,那預(yù)留空間就是老時(shí)代的30%。晉升失敗:新生代做minor gc的時(shí)候,需要CMS的擔(dān)保機(jī)制確認(rèn)老時(shí)代是否有足夠的空間容納要晉升的對(duì)象,擔(dān)保機(jī)制發(fā)現(xiàn)不夠,則報(bào)concurrent mode failure,如果擔(dān)保機(jī)制判斷是夠的,但是事實(shí)上由于碎片問題導(dǎo)致無法分配,就會(huì)報(bào)晉升失敗。永久代空間(或Java8的元空間)耗盡,默認(rèn)狀態(tài)下,CMS不會(huì)對(duì)永久代進(jìn)行獲得,只要永久代空間耗盡,就回觸發(fā)Full GC。三、CMS的調(diào)優(yōu)針對(duì)停頓時(shí)光過長(zhǎng)的調(diào)優(yōu) 首先需要判斷是哪個(gè)階段的停頓導(dǎo)致的,之后跟著再針對(duì)詳細(xì)的原因進(jìn)行調(diào)優(yōu)。使用CMS獲得器的JVM可能引發(fā)停頓的狀態(tài)有:(1)Minor gc的停頓;(2)并發(fā)周期里初始標(biāo)記的停頓;(3)并發(fā)周期里從頭開始標(biāo)記的停頓;(4)Serial-Old獲得老時(shí)代的停頓;(5)Full GC的停頓。之中并發(fā)模式失敗會(huì)導(dǎo)致第(4)種狀態(tài),晉升失敗和永久代空間耗盡會(huì)導(dǎo)致第(5)種狀態(tài)。針對(duì)并發(fā)模式失敗的調(diào)優(yōu)想到一個(gè)辦法增大老時(shí)代的空間,增加整個(gè)堆的大小,或者減少年輕代的大小以更高的頻率執(zhí)行后臺(tái)的回收線程,即提升CMS并發(fā)周期發(fā)生的頻率。設(shè)置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction參數(shù),調(diào)低CMSInitiatingOccupancyFraction的值,但是也不可以調(diào)得太低,太低了會(huì)導(dǎo)致過多的無效的并發(fā)周期,會(huì)導(dǎo)致消耗CPU時(shí)光和再多的無效的停頓。一般來講,這種過程需要幾個(gè)迭代,但是還是有一定的套路,參見《Java性能權(quán)威指南》中給出的反饋,摘抄如下:對(duì)特殊的應(yīng)用軟件程序,該標(biāo)志的更優(yōu)值可以根據(jù) GC 日志中 CMS 周期首次啟動(dòng)失敗時(shí)的值獲得。詳細(xì)途徑是,在垃圾回收日志中找到并發(fā)模式失效,尋找后再反向查找 CMS 周期最近的啟動(dòng)記錄,之后跟著根據(jù)日志來計(jì)算這時(shí)候的老時(shí)代空間占用值,之后跟著設(shè)置一個(gè)比該值更小的值。增多回收線程的個(gè)數(shù) CMS默認(rèn)的垃圾獲得線程數(shù)是(CPU個(gè)數(shù) + 3)/4,這種公式的含義是:當(dāng)CPU個(gè)數(shù)大于4個(gè)的時(shí)候,垃圾回收后臺(tái)線程至少占用25%的CPU資源。舉個(gè)舉例:如果CPU核數(shù)是1-4個(gè),那么會(huì)有1個(gè)CPU用來垃圾獲得,如果CPU核數(shù)是5-8個(gè),那么久會(huì)有2個(gè)CPU用來垃圾獲得。
針對(duì)永久代的調(diào)優(yōu)
如果永久代需要垃圾回收(或元空間擴(kuò)容),就會(huì)觸發(fā)Full GC。默認(rèn)狀態(tài)下,CMS不會(huì)處理永久代中的垃圾,可以通過開啟CMSPermGenSweepingEnabled配置來開啟永久代中的垃圾回收,開啟后會(huì)有一組后臺(tái)線程針對(duì)永久代做獲得,需要小心的是,觸發(fā)永久代進(jìn)行垃圾獲得的指標(biāo)跟觸發(fā)老時(shí)代進(jìn)行垃圾獲得的指標(biāo)是獨(dú)立的,老時(shí)代的閾值可以通過CMSInitiatingPermOccupancyFraction參數(shù)設(shè)置,這種參數(shù)的默認(rèn)值是80%。開啟對(duì)永久代的垃圾獲得只是之中的一步,還需要開啟另外一個(gè)參數(shù)——CMSClassUnloadingEnabled,使得在垃圾獲得的時(shí)候可以卸載不用的類。
四、CMS的trade-off是什么??jī)?yōu)勢(shì)
低延遲的獲得器:幾乎沒有很長(zhǎng)時(shí)間的停頓,應(yīng)用軟件程序只在Minor gc以及后臺(tái)線程掃描老時(shí)代的時(shí)候發(fā)生極其短暫的停頓。劣勢(shì)
更高的CPU使用:一定有足夠的CPU資源用來運(yùn)行后臺(tái)的垃圾獲得線程,在應(yīng)用軟件程序線程運(yùn)行的同一時(shí)間掃描堆的使用狀態(tài)。【PS:現(xiàn)在服務(wù)器的CPU資源基礎(chǔ)不是問題,這種點(diǎn)可以忽略】CMS獲得器對(duì)老時(shí)代獲得的時(shí)候,不再進(jìn)行所有壓縮和整理的事情,說明著老時(shí)代隨著應(yīng)用的運(yùn)行會(huì)變得碎片化;碎片過多會(huì)波及大對(duì)象的分配,即便老時(shí)代還有蠻大的剩余空間,但是沒有連續(xù)的空間來分配大對(duì)象,這時(shí)候就會(huì)觸發(fā)Full GC。CMS提供了兩個(gè)參數(shù)來解決這種問題:(1)UseCMSCompactAtFullCollection,在要進(jìn)行Full GC的時(shí)候進(jìn)行內(nèi)存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不壓縮的Full GC后,執(zhí)行一次帶壓縮的Full GC。會(huì)出現(xiàn)浮動(dòng)垃圾;在并發(fā)清理階段,玩家線程依然在運(yùn)行,一定預(yù)留出空間給玩家線程使用,因此CMS比很多回收器需要更大的堆空間。五、幾個(gè)問題的解答為什么ParNew可以和CMS搭配使用,而Parallel Scanvenge不可以?
答:這種跟Hotspot VM的簡(jiǎn)史有關(guān),Parallel Scanvenge是不在“分代框架”下研究的,而ParNew、CMS都是在分代框架下研究的。
CMS中minor gc和major gc是順序發(fā)生的嗎?
答:不是的,可以交叉發(fā)生,即在并發(fā)周期執(zhí)行過程中,是可以發(fā)生Minor gc的,這種找個(gè)gc日志就可以研究到。
CMS的并發(fā)獲得周期適合觸發(fā)?
由下圖可以就這樣看出,CMS 并發(fā)周期觸發(fā)的條件有兩個(gè):
觸發(fā)cms并發(fā)周期的條件
閾值檢查機(jī)制:老時(shí)代的使用空間達(dá)到某個(gè)閾值,JVM的默認(rèn)值是92%(jdk1.5以前是68%,jdk1.6之后是92%),或者可以通過CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly兩個(gè)參數(shù)來設(shè)置;這種參數(shù)的設(shè)置需要就這樣看應(yīng)用場(chǎng)景,設(shè)置得太小,會(huì)導(dǎo)致CMS頻繁發(fā)生,設(shè)置得太大,會(huì)導(dǎo)致過多的并發(fā)模式失敗。比如
動(dòng)態(tài)檢查機(jī)制:JVM會(huì)根據(jù)最近的回收簡(jiǎn)史,估算下一次老時(shí)代被耗盡的時(shí)光,快到這種時(shí)光的時(shí)候就啟動(dòng)一個(gè)并發(fā)周期。設(shè)置UseCMSInitiatingOccupancyOnly這種參數(shù)可以將這種特性關(guān)閉。
CMS的并發(fā)獲得周期會(huì)掃描哪些對(duì)象?會(huì)回收哪些對(duì)象?
答:CMS的并發(fā)周期只會(huì)回收老時(shí)代的對(duì)象,但是在標(biāo)記老時(shí)代的存活對(duì)象時(shí),可能有的對(duì)象會(huì)被年輕代的對(duì)象引用,因此需要掃描整個(gè)堆的對(duì)象。
CMS的gc roots包括哪些對(duì)象?
答:首先,在JVM垃圾獲得中Gc Roots的概念怎么樣理解(參見R大對(duì)GC roots的概念的解答);第二,CMS的并發(fā)獲得周期中,怎么樣判斷老時(shí)代的對(duì)象是活著?我們前面提到了,在CMS的并發(fā)周期中,僅僅掃描Gc Roots直達(dá)的對(duì)象會(huì)有遺漏,還需要掃描新生代的對(duì)象。如下圖中的藍(lán)色字體所示,CMS中的年輕代和老時(shí)代是分別獲得的,因此在判斷年輕代的對(duì)象存活的時(shí)候,需要把老時(shí)代當(dāng)作自己的GcRoots,這時(shí)候并不需要掃描老時(shí)代的全部對(duì)象,而是使用了card table資料結(jié)構(gòu),如果一個(gè)老時(shí)代對(duì)象引用了年輕代的對(duì)象,則card中的值會(huì)被設(shè)置為特定的數(shù)值;反過來判斷老時(shí)代對(duì)象存活的時(shí)候,也需要把年輕代當(dāng)作自己的Gc Roots,這種過程我們?cè)诘谌?jié)已經(jīng)論述過了。
老時(shí)代和新生代互相作為Gc Roots
如果我的應(yīng)用決定使用CMS獲得器,介紹的JVM參數(shù)是什么?我自己的應(yīng)用使用的參數(shù)如下,是根據(jù)PerfMa的xxfox生成的,各位也完全可以使用這種業(yè)務(wù)調(diào)優(yōu)自己的JVM參數(shù):
CMS有關(guān)的參數(shù)總結(jié)(需要小心的是,這里我沒有思考太多JDK版本的問題,JDK1.7和JDK1.8這些參數(shù)的配置,有的默認(rèn)值可能不一樣,詳細(xì)使用的時(shí)候還需要根據(jù)詳細(xì)的版本來確認(rèn)怎么去調(diào)配設(shè)置)