日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

面试官最常问的垃圾回收器CMS

發(fā)布時(shí)間:2025/3/15 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试官最常问的垃圾回收器CMS 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,線上用戶量的大量增加,性能問(wèn)題變得尤為重要,我們可以通過(guò)增大JVM的各項(xiàng)內(nèi)存來(lái)解決一部分問(wèn)題,但是這樣總是片面的

應(yīng)該雙管齊下,既要從硬件方面變得逐漸強(qiáng)大,底層軟件方向也不能落下發(fā)展,于是乎垃圾收集器的發(fā)展也變得很重要

熟悉JVM的小伙伴應(yīng)該都知道JVM的內(nèi)存結(jié)構(gòu),大致分為堆、棧、本地方法棧、方法區(qū)和程序計(jì)數(shù)器,簡(jiǎn)單回憶下各個(gè)區(qū)域的作用吧

堆:用來(lái)存儲(chǔ)對(duì)象本身的以及數(shù)組(數(shù)組引用是存放在Java棧中的)。堆是被所有線程共享的,在JVM中只有一個(gè)堆

棧:存放的是一個(gè)個(gè)的棧幀,每個(gè)棧幀對(duì)應(yīng)一個(gè)被調(diào)用的方法,在棧幀中包括局部變量表、操作數(shù)棧、指向當(dāng)前方法所屬的類的運(yùn)行時(shí)常量池(運(yùn)行時(shí)常量池的概念在方法區(qū)部分會(huì)談到)的引用

方法返回地址(Return Address)**和一些額外的附加信息。當(dāng)線程執(zhí)行一個(gè)方法時(shí),就會(huì)隨之創(chuàng)建一個(gè)對(duì)應(yīng)的棧幀,并將建立的棧幀壓棧。當(dāng)方法執(zhí)行完畢之后,便會(huì)將棧幀出棧。

本地方法棧:本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過(guò)是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的

方法區(qū):與堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲(chǔ)了每個(gè)類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。在Class文件中除了類的字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用來(lái)存儲(chǔ)編譯期間生成的字面量和符號(hào)引用。

在方法區(qū)中有一個(gè)非常重要的部分就是運(yùn)行時(shí)常量池,它是每一個(gè)類或接口的常量池的運(yùn)行時(shí)表示形式,在類和接口被加載到JVM后,對(duì)應(yīng)的運(yùn)行時(shí)常量池就被創(chuàng)建出來(lái)。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時(shí)常量池,在運(yùn)行期間也可將新的常量放入運(yùn)行時(shí)常量池中,比如String的intern方法。

程序計(jì)數(shù)器:每條線程都有一個(gè)獨(dú)立的的程序計(jì)數(shù)器,各線程間的計(jì)數(shù)器互不影響,因此該區(qū)域是線程私有的。該內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。

好了,我們大概知道了分為這幾大部分,堆和方法區(qū)都會(huì)涉及到垃圾回收,就會(huì)涉及到相應(yīng)的垃圾回收器,就會(huì)有好有壞,或者說(shuō)合適不合適,沒(méi)有最好的,只有最合適的

正文

垃圾回收器都會(huì)涉及到STW的過(guò)程,不知道STW的這里科普一下,stop the world,即STW過(guò)程會(huì)停止所有的用戶線程的執(zhí)行,對(duì)于用戶線程是卡頓的,如果卡頓時(shí)間過(guò)長(zhǎng),用戶會(huì)明顯的感受到反應(yīng)遲鈍

在使用APP的時(shí)候肯定會(huì)有一些卡頓的現(xiàn)象,這種可能有多方面原因,網(wǎng)速,手機(jī)配置,當(dāng)然這些肯定是主要的,也有可能是服務(wù)器正在進(jìn)行STW!

所以咯,對(duì)于開發(fā)人員肯定目標(biāo)就是盡可能的降低STW的時(shí)間,我們今天要說(shuō)的是CMS垃圾回收器,目標(biāo)也會(huì)如此,盡可能的降低應(yīng)用的停頓時(shí)間,這個(gè)目標(biāo)對(duì)于大多數(shù)的交互式應(yīng)用都是很重要的,比如web應(yīng)用;之前我們學(xué)過(guò)的并行收集器組合 Parallel Scavenge + Parallel Old,是以吞吐量為目標(biāo)的垃圾回收器,也是server模式下的默認(rèn)垃圾收集器的配置

我們一起來(lái)看下CMS收集器的工作過(guò)程吧,大致分為七個(gè)步驟:

初始標(biāo)記:為了收集應(yīng)用程序的對(duì)象引用,需要暫停應(yīng)用程序線程,會(huì)導(dǎo)致STW,該階段完成之后應(yīng)用程序再次啟動(dòng)

并發(fā)標(biāo)記:從第一階段到的對(duì)象引用開始,遍歷所有其它的對(duì)象引用

預(yù)清理:第二階段運(yùn)行的時(shí)候,由應(yīng)用程序線程產(chǎn)生的對(duì)象引用,以更新第二階段的結(jié)果

可被終止的預(yù)清理:和用戶線程同時(shí)執(zhí)行的,承擔(dān)下一階段的足夠多的工作

重新標(biāo)記:上一并發(fā)的,對(duì)象引用可能會(huì)發(fā)生進(jìn)一步的改變,因此呢,應(yīng)用程序線程會(huì)再一次被暫停用以更新這些變化,并且在進(jìn)行實(shí)際的清理之前確保一個(gè)正確的對(duì)象引用視圖

并發(fā)清除:所有不再被應(yīng)用的對(duì)象將從堆里清除掉,和用戶線程并行

并發(fā)重置狀態(tài)等待下次CMS的觸發(fā):收集器做一些收尾的工作,以便下一次GC周期能有一個(gè)干凈的狀態(tài)

CMS收集器其實(shí)不是完全和應(yīng)用程序并發(fā)的,我們已經(jīng)看到了,其中也會(huì)有STW的階段,只是相對(duì)來(lái)說(shuō)時(shí)間極其短

詳細(xì)流程

初始標(biāo)記:為了收集應(yīng)用程序的對(duì)象引用,需要暫停應(yīng)用程序線程,會(huì)導(dǎo)致STW,該階段完成之后應(yīng)用程序再次啟動(dòng)

這一步會(huì)發(fā)生STW,這一步的作用是標(biāo)記存活的對(duì)象,包含兩個(gè)部分:

1、老年代中的GC Roots對(duì)象,如圖中的1

2、年輕代中的活著的對(duì)象引用到的老年代對(duì)象

在Java語(yǔ)言里,可作為GC Roots對(duì)象的包括如下幾種:

1、虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對(duì)象?

2、方法區(qū)中的類靜態(tài)屬性引用的對(duì)象?

3、方法區(qū)中的常量引用的對(duì)象?

4、本地方法棧中JNI的引用的對(duì)象

并發(fā)標(biāo)記:從第一階段到的對(duì)象引用開始,遍歷所有其它的對(duì)象引用

從初始標(biāo)記階段標(biāo)記的對(duì)象中找出所有還存活的對(duì)象,因?yàn)槭呛陀脩艟€程并發(fā)運(yùn)行的,在運(yùn)行期間會(huì)有新生代的對(duì)象晉升到老年代,或者說(shuō)直接分配到老年代

對(duì)于這些對(duì)象都需要重新標(biāo)記,否則會(huì)有一些對(duì)象被遺漏的情況,為了提高重新標(biāo)記的效率,該階段會(huì)把上述對(duì)象所在的Card標(biāo)識(shí)為Dirty,后續(xù)則只需要掃描這些Dirty Card的對(duì)象就可以了,不需要掃描整個(gè)老年代了

并發(fā)標(biāo)記階段只是將引用發(fā)生改變的Card標(biāo)記為Dirty狀態(tài),不負(fù)責(zé)清理

由于這個(gè)階段是和用戶線程并發(fā)的,可能會(huì)導(dǎo)致concurrent mode?failure

預(yù)清理:第二階段運(yùn)行的時(shí)候,由應(yīng)用程序線程產(chǎn)生的對(duì)象引用,以更新第二階段的結(jié)果

我們從上一階段其實(shí)沒(méi)有標(biāo)記出老年代的所有存活對(duì)象,是因?yàn)闃?biāo)記的同時(shí)應(yīng)用程序也會(huì)改變一些對(duì)象的引用,這個(gè)階段主要就是用來(lái)處理前一階段因?yàn)橐藐P(guān)系改變導(dǎo)致沒(méi)有標(biāo)記到的存活對(duì)象的

新生代已經(jīng)發(fā)現(xiàn)的引用,比如在并發(fā)階段,在Eden去分配了一個(gè)A對(duì)象,A對(duì)象引用了一個(gè)老年代對(duì)象B,在這個(gè)階段會(huì)標(biāo)記對(duì)象B為活躍對(duì)象

在并發(fā)標(biāo)記階段,如果老年代有對(duì)象內(nèi)部引用發(fā)生變化,而把所在的Card都標(biāo)記為Dirty,通過(guò)掃描這些,重新標(biāo)記那些在并發(fā)階段引用被更新的對(duì)象(晉升到老年代的對(duì)象、原來(lái)在老年代的對(duì)象)

可被終止的預(yù)清理:和用戶線程同時(shí)執(zhí)行的,承擔(dān)下一階段的足夠多的工作

該階段發(fā)生的前提是新生代的Eden的內(nèi)存使用量大于參數(shù)CMSScheduleRemarkEdenSizeThreshold,默認(rèn)是2M,如果新生代的對(duì)象太少,這個(gè)階段沒(méi)必要執(zhí)行,直接執(zhí)行下一階段重新標(biāo)記即可

存在的價(jià)值:盡最大的努力去處理那些在并發(fā)階段被應(yīng)用線程更新的老年代對(duì)象,這樣在暫停的重新標(biāo)記階段就可以少處理一些,暫停時(shí)間會(huì)相應(yīng)的降低些

這個(gè)階段屬于嘗試著去承擔(dān)下一個(gè)階段的部分工作,這個(gè)階段持續(xù)的時(shí)間依賴比較多的因素,這個(gè)階段屬于重復(fù)做相同的事情直到相應(yīng)的條件滿足(次數(shù)、工作量、持續(xù)時(shí)間等

這個(gè)階段主要是循環(huán)做兩件事:

1、處理From和To區(qū)的對(duì)象,標(biāo)記可達(dá)的老年代對(duì)象

2、掃描處理Dirty Card中的對(duì)象

當(dāng)然也肯定不會(huì)一直循環(huán)下去,就像上面說(shuō)的,這里打斷循環(huán)的條件有三個(gè):

最大循環(huán)次數(shù)的設(shè)置CMSMaxAbortablePrecleanLoops,默認(rèn)是0;執(zhí)行的時(shí)間達(dá)到閾值CMSMaxAbortablePrecleanTime,默認(rèn)是5秒;還有一個(gè)就是新生代Eden區(qū)的內(nèi)存使用率達(dá)到閾值CMSScheduleRemarkEdenPenetration,默認(rèn)是50%

重新標(biāo)記:上一并發(fā)的,對(duì)象引用可能會(huì)發(fā)生進(jìn)一步的改變,因此呢,應(yīng)用程序線程會(huì)再一次被暫停用以更新這些變化,并且在進(jìn)行實(shí)際的清理之前確保一個(gè)正確的對(duì)象引用視圖

這一階段是會(huì)發(fā)生STW的,這個(gè)階段的目標(biāo)是完成標(biāo)記整個(gè)老年代的所有存活的對(duì)象,進(jìn)行最后的整理

這個(gè)階段重新標(biāo)記的內(nèi)存范圍是整個(gè)堆,年輕代和老年代都包含

為什么要掃描新生代呢?對(duì)于老年代中的對(duì)象,如果被新生代中的對(duì)象引用就會(huì)被視為存活的對(duì)象,即使新生代中的對(duì)象不可達(dá)了,也會(huì)使用這些不可達(dá)的對(duì)象當(dāng)做GC Roots來(lái)掃描老年代,因此這里要掃描新生代

由于之前的預(yù)處理階段是和用戶線程并發(fā)執(zhí)行的,這時(shí)候可能年輕代的對(duì)象和老年代的引用發(fā)生了很多改變,這時(shí)remark階段可能要花比較多的時(shí)間處理這些改變,會(huì)導(dǎo)致STW,所以通常CMS盡量運(yùn)行Final Remark這一階段的時(shí)候年輕代保持足夠的干凈

這也解釋了上一階段可被終止的預(yù)清理的重要性

并發(fā)清除:所有不再被應(yīng)用的對(duì)象將從堆里清除掉,和用戶線程并行

通過(guò)上面這五個(gè)階段的標(biāo)記,老年代所有存活的對(duì)象已經(jīng)被標(biāo)記,并且現(xiàn)在要通過(guò)Garbage Collector采用清掃的方式回收這些不可用的對(duì)象

這個(gè)階段則主要是清理那些未被標(biāo)記的對(duì)象,回收相應(yīng)的空間

由于這一并發(fā)清楚階段也是和用戶線程同時(shí)運(yùn)行,伴隨著用戶線程的運(yùn)行自然還會(huì)有一些新的垃圾的不斷的產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程之后,CMS自然就無(wú)法在這次回收過(guò)程處理掉這些垃圾,只能等待下一次的GC的時(shí)候才可以清理掉

這一部分被稱為浮動(dòng)垃圾

并發(fā)重置狀態(tài)等待下次CMS的觸發(fā):收集器做一些收尾的工作,以便下一次GC周期能有一個(gè)干凈的狀態(tài)

注意細(xì)節(jié)

隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,線上用戶量的大量增加,性能問(wèn)題變得尤為重要,我們可以通過(guò)增大JVM的各項(xiàng)內(nèi)存來(lái)解決一部分問(wèn)題,但是這樣總是片面的

減少remark階段停頓

一般CMS的GC耗時(shí)80%都在remark階段,如果發(fā)現(xiàn)remark階段停頓時(shí)間很長(zhǎng),可以嘗試添加該參數(shù):-XX:+CMSScavengeBeforeRemark。

在執(zhí)行remark操作之前先做一次Young GC,目的在于減少年輕代對(duì)老年代的無(wú)效引用,降低remark時(shí)的開銷

內(nèi)存碎片問(wèn)題

CMS是基于標(biāo)記-清除算法的,CMS只會(huì)刪除無(wú)用對(duì)象,不會(huì)對(duì)內(nèi)存做壓縮,會(huì)造成內(nèi)存碎片,這時(shí)候我們需要這個(gè)參數(shù):-XX:CMSFullGCsBeforeCompaction=n

意思是說(shuō)在上一次CMS并發(fā)GC執(zhí)行過(guò)后,到底還要再執(zhí)行多少次full GC才會(huì)做壓縮。默認(rèn)是0,也就是在默認(rèn)配置下每次CMS GC頂不住了而要轉(zhuǎn)入full GC的時(shí)候都會(huì)做壓縮。?如果把CMSFullGCsBeforeCompaction配置為10,就會(huì)讓上面說(shuō)的第一個(gè)條件變成每隔10次真正的full GC才做一次壓縮。

concurrent mode failure

這個(gè)異常發(fā)生在cms正在回收的時(shí)候。執(zhí)行CMS GC的過(guò)程中,同時(shí)業(yè)務(wù)線程也在運(yùn)行,當(dāng)年輕帶空間滿了,執(zhí)行ygc時(shí),需要將存活的對(duì)象放入到老年代,而此時(shí)老年代空間不足,這時(shí)CMS還沒(méi)有機(jī)會(huì)回收老年帶產(chǎn)生的,或者在做Minor GC的時(shí)候,新生代救助空間放不下,需要放入老年代,而老年代也放不下而產(chǎn)生的。

設(shè)置cms觸發(fā)時(shí)機(jī)有兩個(gè)參數(shù):-XX:+UseCMSInitiatingOccupancyOnly和-XX:CMSInitiatingOccupancyFraction=70

-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對(duì)內(nèi)存占用率達(dá)到70%的時(shí)候開始GC。

-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用設(shè)定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設(shè)定值,后續(xù)則自動(dòng)調(diào)整會(huì)導(dǎo)致上面的那個(gè)參數(shù)不起作用。

為什么要有這兩個(gè)參數(shù)?

由于在垃圾收集階段用戶線程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。

CMS前五個(gè)階段都是標(biāo)記存活對(duì)象的,除了”初始標(biāo)記”和”重新標(biāo)記”階段會(huì)stop the word ,其它三個(gè)階段都是與用戶線程一起跑的,就會(huì)出現(xiàn)這樣的情況gc線程正在標(biāo)記存活對(duì)象,用戶線程同時(shí)向老年代提升新的對(duì)象,清理工作還沒(méi)有開始,old gen已經(jīng)沒(méi)有空間容納更多對(duì)象了,這時(shí)候就會(huì)導(dǎo)致concurrent mode failure, 然后就會(huì)使用串行收集器回收老年代的垃圾,導(dǎo)致停頓的時(shí)間非常長(zhǎng)。

CMSInitiatingOccupancyFraction參數(shù)要設(shè)置一個(gè)合理的值,設(shè)置大了,會(huì)增加concurrent mode failure發(fā)生的頻率,設(shè)置的小了,又會(huì)增加CMS頻率,所以要根據(jù)應(yīng)用的運(yùn)行情況來(lái)選取一個(gè)合理的值。如果發(fā)現(xiàn)這兩個(gè)參數(shù)設(shè)置大了會(huì)導(dǎo)致full gc,設(shè)置小了會(huì)導(dǎo)致頻繁的CMS GC,說(shuō)明你的老年代空間過(guò)小,應(yīng)該增加老年代空間的大小了。

promotion failed

在進(jìn)行Minor GC時(shí),Survivor Space放不下,對(duì)象只能放入老年代,而此時(shí)老年代也放不下造成的,多數(shù)是由于老年帶有足夠的空閑空間,但是由于碎片較多,新生代要轉(zhuǎn)移到老年帶的對(duì)象比較大,找不到一段連續(xù)區(qū)域存放這個(gè)對(duì)象導(dǎo)致的。

總結(jié)

1、CMS收集器只能收集老年代,其以吞吐量為代價(jià)換取收集速度

2、一共分為七個(gè)步驟,其中初始標(biāo)價(jià)和重新標(biāo)技是STW的,CMS的大部分時(shí)間都花費(fèi)在重新標(biāo)記階段,CMS無(wú)法解決浮動(dòng)垃圾問(wèn)題

3、由于CMS的收集線程和用戶線程并發(fā),可能在收集過(guò)程中出現(xiàn)concurrent mode failure,解決方法就是讓CMS盡早的進(jìn)行GC,在一定次數(shù)的Full GC之后讓CMS對(duì)內(nèi)存做一次壓縮,減少內(nèi)存碎片,防止年輕代對(duì)象晉升到老年代時(shí)因?yàn)閮?nèi)存碎片問(wèn)題導(dǎo)致晉升失敗

有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)

歡迎大家關(guān)注Java之道公眾號(hào)

好文章,我在看??

總結(jié)

以上是生活随笔為你收集整理的面试官最常问的垃圾回收器CMS的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。