前言
之前學(xué)習(xí)JVM垃圾回收時(shí),主要是過了一遍垃圾收集算法野瘦,比如復(fù)制算法描沟,標(biāo)記-清除算法,標(biāo)記-整理算法鞭光,在此基礎(chǔ)上可以增加分代吏廉,每代采取不同的回收算法,以提高整體的分配和回收效率惰许。然后過了一遍JVM中的垃圾收集器席覆,比如Serial、Parallel Scavenge汹买、Parallel New佩伤、CMS聊倔、G1等。
自認(rèn)為垃圾收集就是根據(jù)GC Root標(biāo)記所有可達(dá)的對(duì)象生巡,然后把所有沒有標(biāo)記的對(duì)象清除就ok了耙蔑。是不是很簡(jiǎn)單。事實(shí)上垃圾收集也就是這么一回事孤荣,但是很多時(shí)候說起來簡(jiǎn)單甸陌,做起來卻會(huì)出現(xiàn)很多問題。這篇文章就是記錄我對(duì)CMS垃圾收集器的一些疑問并學(xué)習(xí)的過程垃环。
首先看一下CMS的整體流程(具體每個(gè)流程的詳情就自行了解吧)
如何進(jìn)行標(biāo)記邀层?
最近在看Golang的GC算法實(shí)現(xiàn),里面用到了三色標(biāo)記法遂庄,但是在我的知識(shí)庫中對(duì)三色標(biāo)記法有這個(gè)概念寥院,是的,我只知道這個(gè)概念涛目,不知道三色標(biāo)記法是怎么一個(gè)流程秸谢,也不知道三色標(biāo)記法在GC中怎么與運(yùn)行的。于是就開始了我的探險(xiǎn)之旅霹肝。
在搜索了一下三色標(biāo)記法(具體可以看一下文末參考文檔中三色標(biāo)記法與讀寫屏障了解詳情)后估蹄,發(fā)現(xiàn)現(xiàn)代追蹤式(可達(dá)性分析)的垃圾回收器幾乎都借鑒了三色標(biāo)記的算法思想,CMS垃圾收集器也不例外沫换。
GC Root有哪些臭蚁?
我們知道怎么進(jìn)行標(biāo)記了,但最初標(biāo)記的時(shí)候需要一些根據(jù)才行啊讯赏,這些根據(jù)就是我們說的GC Root垮兑。GC Root有哪些?網(wǎng)上有很多的答案漱挎,我的理解就是
- 當(dāng)前活躍調(diào)用棧中的指向?qū)ο蟮囊?/li>
- 一些不會(huì)發(fā)生改變的數(shù)據(jù)所指向的引用
這里我使用的是引用系枪,而不是對(duì)象,因?yàn)镽大是這樣說的(具體的問題見參考文檔java的gc為什么要分代磕谅?)
所謂“GC roots”私爷,或者說tracing GC的“根集合”,就是一組必須活躍的引用膊夹。
例如說衬浑,這些引用可能包括:
- 所有Java線程當(dāng)前活躍的棧幀里指向GC堆里的對(duì)象的引用;換句話說放刨,當(dāng)前所有正在被調(diào)用的方法的引用類型的參數(shù)/局部變量/臨時(shí)值嚎卫。
- VM的一些靜態(tài)數(shù)據(jù)結(jié)構(gòu)里指向GC堆里的對(duì)象的引用,例如說HotSpot VM里的Universe里有很多這樣的引用。
- JNI handles拓诸,包括global handles和local handles
- (看情況)所有當(dāng)前被加載的Java類
- (看情況)Java類的引用類型靜態(tài)變量
- (看情況)Java類的運(yùn)行時(shí)常量池里的引用類型常量(String或Class類型)
- (看情況)String常量池(StringTable)里的引用
注意,是一組必須活躍的引用麻昼,不是對(duì)象奠支。
現(xiàn)在知道了GC Root,但是我們都知道有分代的概念抚芦,新生代的gc和老年的代的gc回收的區(qū)域是不一樣倍谜,那么這里的GC Root是不是應(yīng)該不一樣呢?肯定是不一樣的叉抡。
首先看一下新生代的GC
新生代的區(qū)域一般都比較小尔崔,而且對(duì)象的存活率都比較低,所以按照前面說的GC Root在新生代的區(qū)域掃描就行了褥民。但是會(huì)有一個(gè)問題季春?老年代存在引用新生代對(duì)象的可能啊消返?如果只掃描新生代的區(qū)域载弄,會(huì)漏掉被老年代引用的對(duì)象,這些對(duì)象就會(huì)被清除掉撵颊,這是不允許的宇攻。
如果這樣的話,那是不是掃描一下老年代的對(duì)象倡勇,看是否引用新生代的對(duì)象是不是就ok了逞刷?嗯這么做肯定是ok的,但是老年代一般很大妻熊,而且存活的對(duì)象很多夸浅,會(huì)導(dǎo)致掃描占用很長(zhǎng)的時(shí)間。那這個(gè)問題如何解固耘?JVM是如何避免Minor GC時(shí)掃描全堆的题篷?
經(jīng)過統(tǒng)計(jì)信息顯示,老年代持有新生代對(duì)象引用的情況不足1%厅目,根據(jù)這一特性JVM引入了卡表(card table)來實(shí)現(xiàn)這一目的番枚。如下圖所示:
卡表的具體策略是將老年代的空間分成大小為512B的若干張卡(card)。卡表本身是單字節(jié)數(shù)組,數(shù)組中的每個(gè)元素對(duì)應(yīng)著一張卡废麻,當(dāng)發(fā)生老年代引用新生代時(shí)甘苍,虛擬機(jī)將該卡對(duì)應(yīng)的卡表元素設(shè)置為適當(dāng)?shù)闹怠H缟蠄D所示并蝗,卡表3被標(biāo)記為臟(卡表還有另外的作用,標(biāo)識(shí)并發(fā)標(biāo)記階段哪些塊被修改過)帘瞭,之后Minor GC時(shí)通過掃描卡表就可以很快的識(shí)別哪些卡中存在老年代指向新生代的引用洋丐。這樣虛擬機(jī)通過空間換時(shí)間的方式呈昔,避免了全堆掃描。
所以新年代GC的GC Root包含2部分
- 新生代中滿足GC Root定義的對(duì)象
- 卡表中老年代引用新生代的對(duì)象
老年代的GC
前面我們說了新生代的gc友绝,我們已同樣的思路來看看老年代的gc堤尾,老年代的GC Root如何來標(biāo)記呢?只掃描老年代可以嗎迁客?當(dāng)然是不行的郭宝,因?yàn)樾律幸部赡艽嬖诶夏甏鷮?duì)象的引用,好在新生代并不大掷漱,所以老年代GC的時(shí)候還需要掃描一遍新生代粘室。
所以老年代GC的GC Root包含2部分
- 老生代中滿足GC Root定義的對(duì)象,如圖節(jié)點(diǎn)1卜范;
- 標(biāo)記年輕代中活著的對(duì)象引用到的老年代的對(duì)象(指的是年輕代中還存活的引用類型對(duì)象衔统,引用指向老年代中的對(duì)象)如圖節(jié)點(diǎn)2、3先朦;
并發(fā)標(biāo)記的好壞?
標(biāo)記作為垃圾回收的第一步缰冤,現(xiàn)在知道如何進(jìn)行標(biāo)記,接下來就是遍歷這些對(duì)象喳魏,將所有未標(biāo)記的對(duì)象清理就完成GC了棉浸。
然而事實(shí)上并沒有這么簡(jiǎn)單,如果標(biāo)記的時(shí)候是STW的刺彩,那就是這么簡(jiǎn)單迷郑,但是如果標(biāo)記過程都STW會(huì)造成暫停時(shí)間過長(zhǎng),給人的感覺就是系統(tǒng)一卡一卡的创倔。
于是就把標(biāo)記的過程改成并發(fā)的進(jìn)行嗡害,也就是CMS中并發(fā)標(biāo)記的過程,然而這就是一切復(fù)雜問題的源頭畦攘。雖然并發(fā)標(biāo)記提升了標(biāo)記的效率霸妹,但是因此卻引發(fā)了一系列的問題。
因?yàn)椴l(fā)標(biāo)記時(shí)知押,gc線程和用戶線程是并行的叹螟,所以在這個(gè)過程中會(huì)出現(xiàn)下面的情況(需要了解三色標(biāo)記法與讀寫屏障):
- 新生代晉升到老年代
- 黑色對(duì)象取消對(duì)灰色對(duì)象的引用(浮動(dòng)垃圾)
- 黑色對(duì)象新增對(duì)白色對(duì)象的引用(漏標(biāo))
其實(shí)在三色標(biāo)記法與讀寫屏障文中已經(jīng)給出了解決方法--添加讀寫屏障
- 寫屏障 + SATB
- 寫屏障 + 增量更新
- 讀屏障(Load Barrier)
在CMS并發(fā)標(biāo)記階段,使用 寫屏障 + 增量更新 的方法台盯,將上面出現(xiàn)的情況標(biāo)記為dirty罢绽,這樣最后再遍歷處理一下Dirty集合中的對(duì)象就ok了
重新標(biāo)記階段為什么還要掃描新生代?
因?yàn)榇嬖?strong>跨代引用静盅,但是前面說過這種情況良价,通過讀寫屏障的方式標(biāo)記這些為dirty,只需要掃描老年代和dirty集合就行了啊明垢?哎蚣常,看來我還是太年輕,如果只掃描老年代和dirty集合會(huì)漏掉一部分袖外,會(huì)是哪部分呢史隆?老年代和dirty集合還沒有覆蓋完嗎?
是的曼验,老年代和dirty集合的確沒有覆蓋完。我們來分析一下粘姜。老年代中經(jīng)過初始標(biāo)記和并發(fā)標(biāo)記后鬓照,只有黑色對(duì)象和白色對(duì)象了,黑色的就是要留下的孤紧,白色的就是要被清除的豺裆。黑色對(duì)象是怎么來的?根據(jù)GC Root找到的号显,所以只要并發(fā)標(biāo)記過程中臭猜,GC Root不發(fā)生變化,黑色對(duì)象就沒有問題(不會(huì)漏標(biāo))押蚤,如果在并發(fā)標(biāo)記過程中GC Root發(fā)生了變化呢蔑歌?
當(dāng)并發(fā)標(biāo)記過程中GC Root增加了,并且這個(gè)GC Root還引用了老年代中的對(duì)象揽碘,此時(shí)如果只掃描老年代和dirty集合就會(huì)漏標(biāo)次屠。因此重新標(biāo)記階段仍然需要掃描新生代。
預(yù)處理階段都干了啥雳刺?
預(yù)處理階段其實(shí)有2部分:
- 預(yù)清理階段
- 可終止的預(yù)處理
這個(gè)階段的目的都是為了減輕后面的重新標(biāo)記的壓力劫灶,提前做一點(diǎn)重新標(biāo)記階段的工作。一般CMS的GC耗時(shí)80%都在remark階段掖桦,所以預(yù)處理階段也是為了減少remark階段的STW時(shí)間本昏。
重新標(biāo)記階段需要做一下工作:
- 遍歷新生代對(duì)象,重新標(biāo)記
- 根據(jù)GC Roots枪汪,重新標(biāo)記
- 遍歷老年代的Dirty Card涌穆,重新標(biāo)記(這里的Dirty Card大部分已經(jīng)在clean階段處理過)
遍歷新生代對(duì)象時(shí),可能很多對(duì)象已經(jīng)是不可達(dá)了料饥,但是還是需要掃描蒲犬。遍歷Dirty Card做處理。
這2部分其實(shí)就是預(yù)處理階段幫助重新標(biāo)記減輕壓力的地方
- 預(yù)清理階段和可終止的預(yù)處理都會(huì)掃描Dirty Card做處理
- 可終止的預(yù)處理岸啡,盡量進(jìn)行一次ygc原叮,讓不可達(dá)的對(duì)象被回收掉,remark階段遍歷新生代的對(duì)象成本小一點(diǎn)
具體這個(gè)階段的詳情見參考文檔圖解CMS垃圾回收機(jī)制,你值得擁有
參考文檔