炸了!一口氣問了我18個(gè)JVM問題!
前言
GC 對(duì)于Java 來說重要性不言而喻,不論是平日里對(duì) JVM 的調(diào)優(yōu)還是面試中的無情轟炸。
這篇文章我會(huì)以一問一答的方式來展開有關(guān) GC 的內(nèi)容。
這篇文章解釋了很多有關(guān)垃圾回收的基本知識(shí),能從源頭上理解垃圾回收和日益發(fā)展的垃圾收集器演進(jìn)的方向,這很重要。
本文章所說的 GC 實(shí)現(xiàn)沒有特殊說明的話,默認(rèn)指的是 HotSpot 的。
我先將十八個(gè)問題都列出來,大家可以先思考下能答出幾道。

好了,開始表演。
young gc、old gc、full gc、mixed gc 傻傻分不清?
這個(gè)問題的前置條件是你得知道 GC 分代,為什么分代。這個(gè)在之前文章提了,不清楚的可以去看看。
現(xiàn)在我們來回答一下這個(gè)問題。
其實(shí) GC 分為兩大類,分別是 Partial GC 和 Full GC。
Partial GC 即部分收集,分為 young gc、old gc、mixed gc。
young gc:指的是單單收集年輕代的 GC。
old gc:指的是單單收集老年代的 GC。
mixed gc:這個(gè)是 G1 收集器特有的,指的是收集整個(gè)年輕代和部分老年代的 GC。
Full GC 即整堆回收,指的是收取整個(gè)堆,包括年輕代、老年代,如果有永久代的話還包括永久代。
其實(shí)還有 Major GC 這個(gè)名詞,在《深入理解Java虛擬機(jī)》中這個(gè)名詞指代的是單單老年代的 GC,也就是和 old gc 等價(jià)的,不過也有很多資料認(rèn)為其是和 full gc 等價(jià)的。
還有 Minor GC,其指的就是年輕代的 gc。
young gc 觸發(fā)條件是什么?
大致上可以認(rèn)為在年輕代的 eden 快要被占滿的時(shí)候會(huì)觸發(fā) young gc。
為什么要說大致上呢?因?yàn)橛幸恍┦占鞯幕厥諏?shí)現(xiàn)是在 full gc 前會(huì)讓先執(zhí)行以下 young gc。
比如 Parallel Scavenge,不過有參數(shù)可以調(diào)整讓其不進(jìn)行 young gc。
可能還有別的實(shí)現(xiàn)也有這種操作,不過正常情況下就當(dāng)做 eden 區(qū)快滿了即可。
eden 快滿的觸發(fā)因素有兩個(gè),一個(gè)是為對(duì)象分配內(nèi)存不夠,一個(gè)是為 TLAB 分配內(nèi)存不夠。
full gc 觸發(fā)條件有哪些?
這個(gè)觸發(fā)條件稍微有點(diǎn)多,我們來看下。
在要進(jìn)行 young gc 的時(shí)候,根據(jù)之前統(tǒng)計(jì)數(shù)據(jù)發(fā)現(xiàn)年輕代平均晉升大小比現(xiàn)在老年代剩余空間要大,那就會(huì)觸發(fā) full gc。
有永久代的話如果永久代滿了也會(huì)觸發(fā) full gc。
老年代空間不足,大對(duì)象直接在老年代申請(qǐng)分配,如果此時(shí)老年代空間不足則會(huì)觸發(fā) full gc。
擔(dān)保失敗即 promotion failure,新生代的 to 區(qū)放不下從 eden 和 from 拷貝過來對(duì)象,或者新生代對(duì)象 gc 年齡到達(dá)閾值需要晉升這兩種情況,老年代如果放不下的話都會(huì)觸發(fā) full gc。
執(zhí)行 System.gc()、jmap -dump 等命令會(huì)觸發(fā) full gc。
知道 TLAB 嗎?來說說看
這個(gè)得從內(nèi)存申請(qǐng)說起。
一般而言生成對(duì)象需要向堆中的新生代申請(qǐng)內(nèi)存空間,而堆又是全局共享的,像新生代內(nèi)存又是規(guī)整的,是通過一個(gè)指針來劃分的。

可想而知如果多個(gè)線程都在分配對(duì)象,那么這個(gè)指針就會(huì)成為熱點(diǎn)資源,需要互斥那分配的效率就低了。
于是搞了個(gè) TLAB(Thread Local Allocation Buffer),為一個(gè)線程分配的內(nèi)存申請(qǐng)區(qū)域。
這個(gè)區(qū)域只允許這一個(gè)線程申請(qǐng)分配對(duì)象,允許所有線程訪問這塊內(nèi)存區(qū)域。
TLAB 的思想其實(shí)很簡單,就是劃一塊區(qū)域給一個(gè)線程,這樣每個(gè)線程只需要在自己的那畝地申請(qǐng)對(duì)象內(nèi)存,不需要爭搶熱點(diǎn)指針。
當(dāng)這塊內(nèi)存用完了之后再去申請(qǐng)即可。
這種思想其實(shí)很常見,比如分布式發(fā)號(hào)器,每次不會(huì)一個(gè)一個(gè)號(hào)的取,會(huì)取一批號(hào),用完之后再去申請(qǐng)一批。

可以看到每個(gè)線程有自己的一塊內(nèi)存分配區(qū)域,短一點(diǎn)的箭頭代表 TLAB 內(nèi)部的分配指針。
如果這塊區(qū)域用完了再去申請(qǐng)即可。
不過每次申請(qǐng)的大小不固定,會(huì)根據(jù)該線程啟動(dòng)到現(xiàn)在的歷史信息來調(diào)整,比如這個(gè)線程一直在分配內(nèi)存那么 TLAB 就大一些,如果這個(gè)線程基本上不會(huì)申請(qǐng)分配內(nèi)存那 TLAB 就小一些。
還有 TLAB 會(huì)浪費(fèi)空間,我們來看下這個(gè)圖。

可以看到 TLAB 內(nèi)部只剩一格大小,申請(qǐng)的對(duì)象需要兩格,這時(shí)候需要再申請(qǐng)一塊 TLAB ,之前的那一格就浪費(fèi)了。
在 HotSpot 中會(huì)生成一個(gè)填充對(duì)象來填滿這一塊,因?yàn)槎研枰€性遍歷,遍歷的流程是通過對(duì)象頭得知對(duì)象的大小,然后跳過這個(gè)大小就能找到下一個(gè)對(duì)象,所以不能有空洞。
當(dāng)然也可以通過空閑鏈表等外部記錄方式來實(shí)現(xiàn)遍歷。
還有 TLAB 只能分配小對(duì)象,大的對(duì)象還是需要在共享的 eden 區(qū)分配。
所以總的來說 TLAB 是為了避免對(duì)象分配時(shí)的競爭而設(shè)計(jì)的。
那 PLAB 知道嗎?
可以看到和 TLAB 很像,PLAB 即 Promotion Local Allocation Buffers。
用在年輕代對(duì)象晉升到老年代時(shí)。
在多線程并行執(zhí)行 YGC 時(shí),可能有很多對(duì)象需要晉升到老年代,此時(shí)老年代的指針就“熱”起來了,于是搞了個(gè) PLAB。
先從老年代 freelist(空閑鏈表) 申請(qǐng)一塊空間,然后在這一塊空間中就可以通過指針加法(bump the pointer)來分配內(nèi)存,這樣對(duì) freelist 競爭也少了,分配空間也快了。

大致就是上圖這么個(gè)思想,每個(gè)線程先申請(qǐng)一塊作為 PLAB ,然后在這一塊內(nèi)存里面分配晉升的對(duì)象。
這和 TLAB 的思想相似。
產(chǎn)生 concurrent mode failure 真正的原因
《深入理解Java虛擬機(jī)》:由于CMS收集器無法處理“浮動(dòng)垃圾”(FloatingGarbage),有可能出現(xiàn)“Con-current Mode Failure”失敗進(jìn)而導(dǎo)致另一次完全“Stop The World”的Full GC的產(chǎn)生。
這段話的意思是因?yàn)閽佭@個(gè)錯(cuò)而導(dǎo)致一次 Full GC。
而實(shí)際上是 Full GC 導(dǎo)致拋這個(gè)錯(cuò),我們來看一下源碼,版本是 openjdk-8。
首先搜一下這個(gè)錯(cuò)。

再找找看 ?report_concurrent_mode_interruption 被誰調(diào)用。
查到是在 void CMSCollector::acquire_control_and_collect(...) 這個(gè)方法中被調(diào)用的。

再來看看 first_state :CollectorState first_state = _collectorState;

看枚舉已經(jīng)很清楚了,就是在 cms gc 還沒結(jié)束的時(shí)候。
而 acquire_control_and_collect 這個(gè)方法是 cms 執(zhí)行 foreground gc 的。
cms 分為 ?foreground gc 和 background gc。
foreground 其實(shí)就是 Full gc。
因此是 full gc 的時(shí)候 cms gc 還在進(jìn)行中導(dǎo)致拋這個(gè)錯(cuò)。
究其原因是因?yàn)榉峙渌俾侍鞂?dǎo)致堆不夠用,回收不過來因此產(chǎn)生 full gc。
也有可能是發(fā)起 cms gc 設(shè)置的堆的閾值太高。
CMS GC 發(fā)生 concurrent mode failure 時(shí)的 full GC 為什么是單線程的?
以下的回答來自 R 大。
因?yàn)闆]足夠開發(fā)資源,偷懶了。就這么簡單。沒有任何技術(shù)上的問題。大公司都自己內(nèi)部做了優(yōu)化。
所以最初怎么會(huì)偷這個(gè)懶的呢?多災(zāi)多難的CMS GC經(jīng)歷了多次動(dòng)蕩。它最初是作為Sun Labs的Exact VM的低延遲GC而設(shè)計(jì)實(shí)現(xiàn)的。
但 Exact VM在與 HotSpot VM爭搶 Sun 的正牌 JVM 的內(nèi)部斗爭中失利,CMS GC 后來就作為 Exact VM 的技術(shù)遺產(chǎn)被移植到了 HotSpot VM上。
就在這個(gè)移植還在進(jìn)行中的時(shí)候,Sun 已經(jīng)開始略顯疲態(tài);到 CMS GC 完全移植到 HotSpot VM 的時(shí)候,Sun 已經(jīng)處于快要不行的階段了。
開發(fā)資源減少,開發(fā)人員流失,當(dāng)時(shí)的 HotSpot VM 開發(fā)組能夠做的事情并不多,只能挑重要的來做。而這個(gè)時(shí)候 Sun Labs 的另一個(gè) GC 實(shí)現(xiàn),Garbage-First GC(G1 GC)已經(jīng)面世。
相比可能在長時(shí)間運(yùn)行后受碎片化影響的 CMS,G1 會(huì)增量式的整理/壓縮堆里的數(shù)據(jù),避免受碎片化影響,因而被認(rèn)為更具潛力。
于是當(dāng)時(shí)本來就不多的開發(fā)資源,一部分還投給了把G1 GC產(chǎn)品化的項(xiàng)目上——結(jié)果也是進(jìn)展緩慢。
畢竟只有一兩個(gè)人在做。所以當(dāng)時(shí)就沒能有足夠開發(fā)資源去打磨 CMS GC 的各種配套設(shè)施的細(xì)節(jié),配套的備份 full GC 的并行化也就耽擱了下來。
但肯定會(huì)有同學(xué)抱有疑問:HotSpot VM不是已經(jīng)有并行GC了么?而且還有好幾個(gè)?
讓我們來看看:
ParNew:并行的young gen GC,不負(fù)責(zé)收集old gen。
Parallel GC(ParallelScavenge):并行的young gen GC,與ParNew相似但不兼容;同樣不負(fù)責(zé)收集old gen。
ParallelOld GC(PSCompact):并行的full GC,但與ParNew / CMS不兼容。
所以…就是這么一回事。
HotSpot VM 確實(shí)是已經(jīng)有并行 GC 了,但兩個(gè)是只負(fù)責(zé)在 young GC 時(shí)收集 young gen 的,這倆之中還只有 ParNew 能跟 CMS 搭配使用;
而并行 full GC 雖然有一個(gè) ParallelOld,但卻與 CMS GC 不兼容所以無法作為它的備份 full GC使用。
為什么有些新老年代的收集器不能組合使用比如 ParNew 和 Parallel Old?

這張圖是 2008 年 HostSpot 一位 GC 組成員畫的,那時(shí)候 G1 還沒問世,在研發(fā)中,所以畫了個(gè)問號(hào)在上面。
里面的回答是 :
"ParNew" is written in a style... "Parallel Old" is not written in the "ParNew" style
HotSpot VM 自身的分代收集器實(shí)現(xiàn)有一套框架,只有在框架內(nèi)的實(shí)現(xiàn)才能互相搭配使用。
而有個(gè)開發(fā)他不想按照這個(gè)框架實(shí)現(xiàn),自己寫了個(gè),測(cè)試的成績還不錯(cuò)后來被 ?HotSpot VM 給吸收了,這就導(dǎo)致了不兼容。
我之前看到一個(gè)回答解釋的很形象:就像動(dòng)車組車頭帶不了綠皮車廂一樣,電氣,掛鉤啥的都不匹配。
新生代的 GC 如何避免全堆掃描?
在常見的分代 GC 中就是利用記憶集來實(shí)現(xiàn)的,記錄可能存在的老年代中有新生代的引用的對(duì)象地址,來避免全堆掃描。

上圖有個(gè)對(duì)象精度的,一個(gè)是卡精度的,卡精度的叫卡表。
把堆中分為很多塊,每塊 512 字節(jié)(卡頁),用字節(jié)數(shù)組來中的一個(gè)元素來表示某一塊,1表示臟塊,里面存在跨代引用。

在 Hotspot 中的實(shí)現(xiàn)是卡表,是通過寫后屏障維護(hù)的,偽代碼如下。

cms 中需要記錄老年代指向年輕代的引用,但是寫屏障的實(shí)現(xiàn)并沒有做任何條件的過濾。
即不判斷當(dāng)前對(duì)象是老年代對(duì)象且引用的是新生代對(duì)象才會(huì)標(biāo)記對(duì)應(yīng)的卡表為臟。
只要是引用賦值都會(huì)把對(duì)象的卡標(biāo)記為臟,當(dāng)然YGC掃描的時(shí)候只會(huì)掃老年代的卡表。
這樣做是減少寫屏障帶來的消耗,畢竟引用的賦值非常的頻繁。
那 cms 的記憶集和 G1 的記憶集有什么不一樣?
cms 的記憶集的實(shí)現(xiàn)是卡表即 card table。
通常實(shí)現(xiàn)的記憶集是 points-out 的,我們知道記憶集是用來記錄非收集區(qū)域指向收集區(qū)域的跨代引用,它的主語其實(shí)是非收集區(qū)域,所以是 points-out 的。
在 cms 中只有老年代指向年輕代的卡表,用于年輕代 gc。
而 G1 是基于 region 的,所以在 points-out 的卡表之上還加了個(gè) points-into 的結(jié)構(gòu)。
因?yàn)橐粋€(gè) region 需要知道有哪些別的 region 有指向自己的指針,然后還需要知道這些指針在哪些 card 中。
其實(shí) G1 的記憶集就是個(gè) hash table,key 就是別的 region 的起始地址,然后 value 是一個(gè)集合,里面存儲(chǔ)這 card table 的 index。
我們來看下這個(gè)圖就很清晰了。

像每次引用字段的賦值都需要維護(hù)記憶集開銷很大,所以 G1 的實(shí)現(xiàn)利用了 logging write barrier(下文會(huì)介紹)。
也是異步思想,會(huì)先將修改記錄到隊(duì)列中,當(dāng)隊(duì)列超過一定閾值由后臺(tái)線程取出遍歷來更新記憶集。
為什么 G1 不維護(hù)年輕代到老年代的記憶集?
G1 分了 young GC 和 mixed gc。
young gc 會(huì)選取所有年輕代的 region 進(jìn)行收集。
midex gc 會(huì)選取所有年輕代的 region 和一些收集收益高的老年代 region 進(jìn)行收集。
所以年輕代的 region 都在收集范圍內(nèi),所以不需要額外記錄年輕代到老年代的跨代引用。
cms 和 G1 為了維持并發(fā)的正確性分別用了什么手段?
之前文章分析到了并發(fā)執(zhí)行漏標(biāo)的兩個(gè)充分必要條件是:
將新對(duì)象插入已掃描完畢的對(duì)象中,即插入黑色對(duì)象到白色對(duì)象的引用。 刪除了灰色對(duì)象到白色對(duì)象的引用。

什么是 logging write barrier ?

簡單說下 G1 回收流程
簡單說下 cms 回收流程
cms 寫屏障又是維護(hù)卡表,又得維護(hù)增量更新?
GC 調(diào)優(yōu)的兩大目標(biāo)是啥?
GC 如何調(diào)優(yōu)
巨人的肩膀
https://segmentfault.com/a/1190000021394215?utm_source=tag-newest
https://blogs.oracle.com/jonthecollector/our-collectors
https://www.iteye.com/blog/user/rednaxelafx R大的博客
https://www.jianshu.com/u/90ab66c248e6 占小狼的博客
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
