面试官最常问的垃圾回收器CMS
前言
隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,線上用戶量的大量增加,性能問題變得尤為重要,我們可以通過增大JVM的各項內(nèi)存來解決一部分問題,但是這樣總是片面的
應(yīng)該雙管齊下,既要從硬件方面變得逐漸強(qiáng)大,底層軟件方向也不能落下發(fā)展,于是乎垃圾收集器的發(fā)展也變得很重要
熟悉JVM的小伙伴應(yīng)該都知道JVM的內(nèi)存結(jié)構(gòu),大致分為堆、棧、本地方法棧、方法區(qū)和程序計數(shù)器,簡單回憶下各個區(qū)域的作用吧
堆:用來存儲對象本身的以及數(shù)組(數(shù)組引用是存放在Java棧中的)。堆是被所有線程共享的,在JVM中只有一個堆
棧:存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法,在棧幀中包括局部變量表、操作數(shù)棧、指向當(dāng)前方法所屬的類的運(yùn)行時常量池(運(yùn)行時常量池的概念在方法區(qū)部分會談到)的引用
方法返回地址(Return Address)**和一些額外的附加信息。當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀,并將建立的棧幀壓棧。當(dāng)方法執(zhí)行完畢之后,便會將棧幀出棧。
本地方法棧:本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的
方法區(qū):與堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區(qū)中有一個非常重要的部分就是運(yùn)行時常量池,它是每一個類或接口的常量池的運(yùn)行時表示形式,在類和接口被加載到JVM后,對應(yīng)的運(yùn)行時常量池就被創(chuàng)建出來。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時常量池,在運(yùn)行期間也可將新的常量放入運(yùn)行時常量池中,比如String的intern方法。
程序計數(shù)器:每條線程都有一個獨(dú)立的的程序計數(shù)器,各線程間的計數(shù)器互不影響,因此該區(qū)域是線程私有的。該內(nèi)存區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。
好了,我們大概知道了分為這幾大部分,堆和方法區(qū)都會涉及到垃圾回收,就會涉及到相應(yīng)的垃圾回收器,就會有好有壞,或者說合適不合適,沒有最好的,只有最合適的
正文
垃圾回收器都會涉及到STW的過程,不知道STW的這里科普一下,stop the world,即STW過程會停止所有的用戶線程的執(zhí)行,對于用戶線程是卡頓的,如果卡頓時間過長,用戶會明顯的感受到反應(yīng)遲鈍
在使用APP的時候肯定會有一些卡頓的現(xiàn)象,這種可能有多方面原因,網(wǎng)速,手機(jī)配置,當(dāng)然這些肯定是主要的,也有可能是服務(wù)器正在進(jìn)行STW!
所以咯,對于開發(fā)人員肯定目標(biāo)就是盡可能的降低STW的時間,我們今天要說的是CMS垃圾回收器,目標(biāo)也會如此,盡可能的降低應(yīng)用的停頓時間,這個目標(biāo)對于大多數(shù)的交互式應(yīng)用都是很重要的,比如web應(yīng)用;之前我們學(xué)過的并行收集器組合 Parallel Scavenge + Parallel Old,是以吞吐量為目標(biāo)的垃圾回收器,也是server模式下的默認(rèn)垃圾收集器的配置
我們一起來看下CMS收集器的工作過程吧,大致分為七個步驟:
初始標(biāo)記:為了收集應(yīng)用程序的對象引用,需要暫停應(yīng)用程序線程,會導(dǎo)致STW,該階段完成之后應(yīng)用程序再次啟動
并發(fā)標(biāo)記:從第一階段到的對象引用開始,遍歷所有其它的對象引用
預(yù)清理:第二階段運(yùn)行的時候,由應(yīng)用程序線程產(chǎn)生的對象引用,以更新第二階段的結(jié)果
可被終止的預(yù)清理:和用戶線程同時執(zhí)行的,承擔(dān)下一階段的足夠多的工作
重新標(biāo)記:上一并發(fā)的,對象引用可能會發(fā)生進(jìn)一步的改變,因此呢,應(yīng)用程序線程會再一次被暫停用以更新這些變化,并且在進(jìn)行實(shí)際的清理之前確保一個正確的對象引用視圖
并發(fā)清除:所有不再被應(yīng)用的對象將從堆里清除掉,和用戶線程并行
并發(fā)重置狀態(tài)等待下次CMS的觸發(fā):收集器做一些收尾的工作,以便下一次GC周期能有一個干凈的狀態(tài)
CMS收集器其實(shí)不是完全和應(yīng)用程序并發(fā)的,我們已經(jīng)看到了,其中也會有STW的階段,只是相對來說時間極其短
詳細(xì)流程
初始標(biāo)記:為了收集應(yīng)用程序的對象引用,需要暫停應(yīng)用程序線程,會導(dǎo)致STW,該階段完成之后應(yīng)用程序再次啟動
這一步會發(fā)生STW,這一步的作用是標(biāo)記存活的對象,包含兩個部分:
1、老年代中的GC Roots對象,如圖中的1
2、年輕代中的活著的對象引用到的老年代對象
在Java語言里,可作為GC Roots對象的包括如下幾種:
1、虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對象?
2、方法區(qū)中的類靜態(tài)屬性引用的對象?
3、方法區(qū)中的常量引用的對象?
4、本地方法棧中JNI的引用的對象
并發(fā)標(biāo)記:從第一階段到的對象引用開始,遍歷所有其它的對象引用
從初始標(biāo)記階段標(biāo)記的對象中找出所有還存活的對象,因為是和用戶線程并發(fā)運(yùn)行的,在運(yùn)行期間會有新生代的對象晉升到老年代,或者說直接分配到老年代
對于這些對象都需要重新標(biāo)記,否則會有一些對象被遺漏的情況,為了提高重新標(biāo)記的效率,該階段會把上述對象所在的Card標(biāo)識為Dirty,后續(xù)則只需要掃描這些Dirty Card的對象就可以了,不需要掃描整個老年代了
并發(fā)標(biāo)記階段只是將引用發(fā)生改變的Card標(biāo)記為Dirty狀態(tài),不負(fù)責(zé)清理
由于這個階段是和用戶線程并發(fā)的,可能會導(dǎo)致concurrent mode?failure
預(yù)清理:第二階段運(yùn)行的時候,由應(yīng)用程序線程產(chǎn)生的對象引用,以更新第二階段的結(jié)果
我們從上一階段其實(shí)沒有標(biāo)記出老年代的所有存活對象,是因為標(biāo)記的同時應(yīng)用程序也會改變一些對象的引用,這個階段主要就是用來處理前一階段因為引用關(guān)系改變導(dǎo)致沒有標(biāo)記到的存活對象的
新生代已經(jīng)發(fā)現(xiàn)的引用,比如在并發(fā)階段,在Eden去分配了一個A對象,A對象引用了一個老年代對象B,在這個階段會標(biāo)記對象B為活躍對象
在并發(fā)標(biāo)記階段,如果老年代有對象內(nèi)部引用發(fā)生變化,而把所在的Card都標(biāo)記為Dirty,通過掃描這些,重新標(biāo)記那些在并發(fā)階段引用被更新的對象(晉升到老年代的對象、原來在老年代的對象)
可被終止的預(yù)清理:和用戶線程同時執(zhí)行的,承擔(dān)下一階段的足夠多的工作
該階段發(fā)生的前提是新生代的Eden的內(nèi)存使用量大于參數(shù)CMSScheduleRemarkEdenSizeThreshold,默認(rèn)是2M,如果新生代的對象太少,這個階段沒必要執(zhí)行,直接執(zhí)行下一階段重新標(biāo)記即可
存在的價值:盡最大的努力去處理那些在并發(fā)階段被應(yīng)用線程更新的老年代對象,這樣在暫停的重新標(biāo)記階段就可以少處理一些,暫停時間會相應(yīng)的降低些
這個階段屬于嘗試著去承擔(dān)下一個階段的部分工作,這個階段持續(xù)的時間依賴比較多的因素,這個階段屬于重復(fù)做相同的事情直到相應(yīng)的條件滿足(次數(shù)、工作量、持續(xù)時間等
這個階段主要是循環(huán)做兩件事:
1、處理From和To區(qū)的對象,標(biāo)記可達(dá)的老年代對象
2、掃描處理Dirty Card中的對象
當(dāng)然也肯定不會一直循環(huán)下去,就像上面說的,這里打斷循環(huán)的條件有三個:
最大循環(huán)次數(shù)的設(shè)置CMSMaxAbortablePrecleanLoops,默認(rèn)是0;執(zhí)行的時間達(dá)到閾值CMSMaxAbortablePrecleanTime,默認(rèn)是5秒;還有一個就是新生代Eden區(qū)的內(nèi)存使用率達(dá)到閾值CMSScheduleRemarkEdenPenetration,默認(rèn)是50%
重新標(biāo)記:上一并發(fā)的,對象引用可能會發(fā)生進(jìn)一步的改變,因此呢,應(yīng)用程序線程會再一次被暫停用以更新這些變化,并且在進(jìn)行實(shí)際的清理之前確保一個正確的對象引用視圖
這一階段是會發(fā)生STW的,這個階段的目標(biāo)是完成標(biāo)記整個老年代的所有存活的對象,進(jìn)行最后的整理
這個階段重新標(biāo)記的內(nèi)存范圍是整個堆,年輕代和老年代都包含
為什么要掃描新生代呢?對于老年代中的對象,如果被新生代中的對象引用就會被視為存活的對象,即使新生代中的對象不可達(dá)了,也會使用這些不可達(dá)的對象當(dāng)做GC Roots來掃描老年代,因此這里要掃描新生代
由于之前的預(yù)處理階段是和用戶線程并發(fā)執(zhí)行的,這時候可能年輕代的對象和老年代的引用發(fā)生了很多改變,這時remark階段可能要花比較多的時間處理這些改變,會導(dǎo)致STW,所以通常CMS盡量運(yùn)行Final Remark這一階段的時候年輕代保持足夠的干凈
這也解釋了上一階段可被終止的預(yù)清理的重要性
并發(fā)清除:所有不再被應(yīng)用的對象將從堆里清除掉,和用戶線程并行
通過上面這五個階段的標(biāo)記,老年代所有存活的對象已經(jīng)被標(biāo)記,并且現(xiàn)在要通過Garbage Collector采用清掃的方式回收這些不可用的對象
這個階段則主要是清理那些未被標(biāo)記的對象,回收相應(yīng)的空間
由于這一并發(fā)清楚階段也是和用戶線程同時運(yùn)行,伴隨著用戶線程的運(yùn)行自然還會有一些新的垃圾的不斷的產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS自然就無法在這次回收過程處理掉這些垃圾,只能等待下一次的GC的時候才可以清理掉
這一部分被稱為浮動垃圾
并發(fā)重置狀態(tài)等待下次CMS的觸發(fā):收集器做一些收尾的工作,以便下一次GC周期能有一個干凈的狀態(tài)
注意細(xì)節(jié)
隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,線上用戶量的大量增加,性能問題變得尤為重要,我們可以通過增大JVM的各項內(nèi)存來解決一部分問題,但是這樣總是片面的
減少remark階段停頓
一般CMS的GC耗時80%都在remark階段,如果發(fā)現(xiàn)remark階段停頓時間很長,可以嘗試添加該參數(shù):-XX:+CMSScavengeBeforeRemark。
在執(zhí)行remark操作之前先做一次Young GC,目的在于減少年輕代對老年代的無效引用,降低remark時的開銷
內(nèi)存碎片問題
CMS是基于標(biāo)記-清除算法的,CMS只會刪除無用對象,不會對內(nèi)存做壓縮,會造成內(nèi)存碎片,這時候我們需要這個參數(shù):-XX:CMSFullGCsBeforeCompaction=n
意思是說在上一次CMS并發(fā)GC執(zhí)行過后,到底還要再執(zhí)行多少次full GC才會做壓縮。默認(rèn)是0,也就是在默認(rèn)配置下每次CMS GC頂不住了而要轉(zhuǎn)入full GC的時候都會做壓縮。?如果把CMSFullGCsBeforeCompaction配置為10,就會讓上面說的第一個條件變成每隔10次真正的full GC才做一次壓縮。
concurrent mode failure
這個異常發(fā)生在cms正在回收的時候。執(zhí)行CMS GC的過程中,同時業(yè)務(wù)線程也在運(yùn)行,當(dāng)年輕帶空間滿了,執(zhí)行ygc時,需要將存活的對象放入到老年代,而此時老年代空間不足,這時CMS還沒有機(jī)會回收老年帶產(chǎn)生的,或者在做Minor GC的時候,新生代救助空間放不下,需要放入老年代,而老年代也放不下而產(chǎn)生的。
設(shè)置cms觸發(fā)時機(jī)有兩個參數(shù):-XX:+UseCMSInitiatingOccupancyOnly和-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對內(nèi)存占用率達(dá)到70%的時候開始GC。
-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用設(shè)定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設(shè)定值,后續(xù)則自動調(diào)整會導(dǎo)致上面的那個參數(shù)不起作用。
為什么要有這兩個參數(shù)?
由于在垃圾收集階段用戶線程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時的程序運(yùn)作使用。
CMS前五個階段都是標(biāo)記存活對象的,除了”初始標(biāo)記”和”重新標(biāo)記”階段會stop the word ,其它三個階段都是與用戶線程一起跑的,就會出現(xiàn)這樣的情況gc線程正在標(biāo)記存活對象,用戶線程同時向老年代提升新的對象,清理工作還沒有開始,old gen已經(jīng)沒有空間容納更多對象了,這時候就會導(dǎo)致concurrent mode failure, 然后就會使用串行收集器回收老年代的垃圾,導(dǎo)致停頓的時間非常長。
CMSInitiatingOccupancyFraction參數(shù)要設(shè)置一個合理的值,設(shè)置大了,會增加concurrent mode failure發(fā)生的頻率,設(shè)置的小了,又會增加CMS頻率,所以要根據(jù)應(yīng)用的運(yùn)行情況來選取一個合理的值。如果發(fā)現(xiàn)這兩個參數(shù)設(shè)置大了會導(dǎo)致full gc,設(shè)置小了會導(dǎo)致頻繁的CMS GC,說明你的老年代空間過小,應(yīng)該增加老年代空間的大小了。
promotion failed
在進(jìn)行Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下造成的,多數(shù)是由于老年帶有足夠的空閑空間,但是由于碎片較多,新生代要轉(zhuǎn)移到老年帶的對象比較大,找不到一段連續(xù)區(qū)域存放這個對象導(dǎo)致的。
總結(jié)
1、CMS收集器只能收集老年代,其以吞吐量為代價換取收集速度
2、一共分為七個步驟,其中初始標(biāo)價和重新標(biāo)技是STW的,CMS的大部分時間都花費(fèi)在重新標(biāo)記階段,CMS無法解決浮動垃圾問題
3、由于CMS的收集線程和用戶線程并發(fā),可能在收集過程中出現(xiàn)concurrent mode failure,解決方法就是讓CMS盡早的進(jìn)行GC,在一定次數(shù)的Full GC之后讓CMS對內(nèi)存做一次壓縮,減少內(nèi)存碎片,防止年輕代對象晉升到老年代時因為內(nèi)存碎片問題導(dǎo)致晉升失敗
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的面试官最常问的垃圾回收器CMS的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: synchronized 底层了解一下.
- 下一篇: 用hundred造句子_2020朋友圈感