最近,五哥回憶起4年前在螞蟻金服三面的經(jīng)歷。關(guān)于GC的一個(gè)問題,讓我記憶深刻。
當(dāng)聊起來Java GC時(shí),我提到 young gc 和 full gc都會 Stop the world。
”為什么需要 Stop the world“,螞蟻面試官問道。
我略微怔住,想了一會,回答道:“如果一邊垃圾回收,業(yè)務(wù)線程一邊跑,可能清理的不干凈吧,也可能有一些對象被錯誤回收”。 事實(shí)上,我沒背過這個(gè)八股文,只能憑感覺瞎說。本以為這個(gè)問題就這么糊弄過去了。
“你可以舉例說明下,為什么GC需要Stop the world嗎?舉個(gè)例子說明下?” ,螞蟻面試?yán)^續(xù)追問道。
這個(gè)追問讓我措手不及,一臉懵逼。想了幾分鐘后,半天也沒放出個(gè)屁…… 支支吾吾沒答出來,只得坦白這個(gè)問題沒有想清楚,我不會。
大家都明白,為什么需要Stop the world,但要舉一個(gè)實(shí)際例子來證明這一點(diǎn)有難度,就像大家都了解快速排序的原理,但手寫非遞歸版本的快速排序真的很困難。
后來的我花了很久,才想到兩個(gè)例子來反證這個(gè)結(jié)論。在此之前,我先啰嗦一下,故事的背景……
故事的背景
2019年的春天,剛畢業(yè)兩年的我,在忙著換工作,那時(shí)候的求職環(huán)境還不像今天這么寒冷,大公司的面試機(jī)會有很多,即便如此,我也非常珍惜大公司的面試機(jī)會,尤其是螞蟻金服。那一天,我坐地鐵10號線,去朝陽區(qū)螞蟻金服的辦公點(diǎn)——環(huán)球金融中心 現(xiàn)場面試,在我看來,這個(gè)大樓的名字和螞蟻金服四個(gè)字一樣,非常高大上。算上電話面試,今天是第三面,如果面試通過,不出意外的話,后面沒有技術(shù)面了,應(yīng)該就能拿到Offer。 心想著,終于可以拿到一個(gè)滿意的Offer了, 于是既興奮又緊張的走進(jìn)了螞蟻金服。
我沒有想到,兩個(gè)小時(shí)后,我會灰溜溜的出來~
當(dāng)時(shí)的我準(zhǔn)備得十分充分,我先用小公司、中型公司的面試機(jī)會練手,像螞蟻金服類的大公司留到最后再面試,目的是一擊必中,不留遺憾。在當(dāng)時(shí),我已經(jīng)有十幾輪的面試經(jīng)驗(yàn)了,自我介紹和項(xiàng)目介紹背的滾瓜爛熟,常見八股文,簡單leetCode完全難不倒我。甭管實(shí)力如何,至少在心理上我十分自信。
然而螞蟻金服的面試官從淺入深,在各方面盤問我的技術(shù)實(shí)力。我的印象是:他們從一個(gè)點(diǎn)開始問,一直問到底,問到我不會為止,我一度接近崩潰……。這些問題我努力回憶了一下,可以 # 點(diǎn)擊查看8家大廠后端面試題
為什么需要Stop the world
比較官方的解釋如下
分析工作必須在一個(gè)能確保一致性的快照中進(jìn)行
一致性指整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上
如果出現(xiàn)分析過程中對象引用關(guān)系還在不斷地變化,則分析結(jié)果的準(zhǔn)確性無法保證。
官方給的解釋中,重點(diǎn)在強(qiáng)調(diào),GC的分析要確保在一個(gè)一致性的視圖之上,否則無法保證垃圾回收的準(zhǔn)確性。這和我說的幾乎一樣,“有一些對象可能會被錯誤回收”,只不過官方的說法更加專業(yè)。但是官方并沒有給出具體的例子…… 這需要我們自己探索。
垃圾回收算法中的標(biāo)記工作
在Java堆中,存放著所有Java的對象實(shí)例。在進(jìn)行垃圾收集之前,JVM需要確定哪些對象已經(jīng)不被使用(即垃圾),哪些對象仍然被使用。為了判斷對象是否是“垃圾”,JVM采用了可達(dá)性分析算法。
可達(dá)性分析算法 是指通過指定 GC Root 根對象,從根對象開始搜索引用的對象,通過引用鏈條,層層遍歷鏈條上的對象,可以到達(dá)的對象不可被垃圾回收。而最終沒有被搜索遍歷到的對象,則為 不可達(dá)對象,應(yīng)該被垃圾回收。
JVM中的 GC Root根對象包括如下:
- 虛擬機(jī)棧引用的對象
- 本地方法棧內(nèi)JNI(本地方法)引用的對象
- 方法區(qū)中常量引用的對象(字符串常量池)
- 所有被同步鎖synchronized持有的對象
- Java虛擬機(jī)內(nèi)部的引用
垃圾回收必須要先標(biāo)記出垃圾對象,才可以進(jìn)行后續(xù)的清理工作。無論是采用 標(biāo)記-整理算法還是標(biāo)記-清理算法。標(biāo)記工作都是必不可少的。
如上文指出,JVM 明確標(biāo)記工作進(jìn)行時(shí),業(yè)務(wù)線程必須暫停執(zhí)行!
下面我提出兩個(gè)例子說明下,如果業(yè)務(wù)線程沒有被暫停,會造成什么后果!
使用反證法證明,為什么需要Stop the world
接下來,我通過一段代碼,證明這個(gè)結(jié)論: 如果不暫停業(yè)務(wù)線程,對象會被錯誤的垃圾回收!

以上代碼中,聲明了 target static靜態(tài)常量,引用了Context類型對象。因?yàn)楸怀A恳?,target在GC Root上。也就是說垃圾回收時(shí),會以target為根,開始遍歷。正常情況下target引用的對象不會被回收…… 但如果不暫停業(yè)務(wù)線程后,Context對象會被錯誤回收!
在main方法中一共有 4 步。
首先 第一步:定義 temp變量為 null;
第二步:將target引用賦值給 temp;
第三步:將target 變量指向null,此時(shí)Context對象只被 temp 變量引用。
最后一步第四步,調(diào)用temp.toString();
接下里我開始分析,假設(shè)開啟垃圾回收時(shí),不暫停業(yè)務(wù)線程,垃圾回收線程和業(yè)務(wù)線程一起并發(fā)執(zhí)行,會有哪些潛在的坑點(diǎn)!
為了清晰期間,我使用表格來表示時(shí)間線。在此例中,兩個(gè)線程為 main 線程和垃圾回收標(biāo)記線程。

通過上圖的分析,我們發(fā)現(xiàn),原Context對象,在被其他變量引用的情況下,被錯誤的垃圾回收,造成了不可預(yù)測的情況發(fā)生。
原因是,當(dāng)垃圾回收線程檢查 main 線程時(shí),發(fā)現(xiàn)無法通過 main 線程的虛擬機(jī)棧引用 Context 對象。這是因?yàn)?temp 還未被賦值。于是,垃圾回收線程轉(zhuǎn)而遍歷 target 變量。在此時(shí)間窗口內(nèi),main 線程從 target 變量中獲取到 Context 對象的引用,并將 target 變量設(shè)置為null?;氐嚼厥站€程,它檢查到 target 變量為null。在垃圾回收線程看來,無論是main線程還是 target 變量,都沒有引用到 Context 對象。因此,垃圾回收器回收了 Context 對象。然而,main 線程中的 temp 變量仍然持有 Context 對象。在這種情況下,對 Context 對象的任何操作都將變得不可預(yù)測。
這個(gè)過程略微有些混亂,可以通過參考時(shí)間線表格來更好地理解。
此時(shí)再去理解 JVM官方文檔給的原因
分析工作必須在一個(gè)能確保一致性的快照中進(jìn)行,一致性指整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上
正是因?yàn)?main 線程和垃圾回收線程同時(shí)執(zhí)行,導(dǎo)致垃圾回收在分析 main 線程的虛擬機(jī)棧和target變量時(shí),并沒有在一個(gè)凍結(jié)的時(shí)間上,而是在先后的兩個(gè)時(shí)間點(diǎn)分析對象是否可達(dá)。在先后時(shí)間的窗口期內(nèi),Context 對象被賦值給其他對象,但是垃圾回收線程對此毫無感知…… 最終當(dāng) Context 被錯誤回收以后,業(yè)務(wù)線程訪問 Context時(shí),將出現(xiàn)極其詭異且致命的問題……
通過反證法,我們證明出,如果不暫停業(yè)務(wù)線程,那么無法進(jìn)行準(zhǔn)確的垃圾回收工作。
其他例子
還有其他例子可以佐證。
例如只有兩行代碼 的一段程序。
Context temp = null;
temp = new Context();`
main 線程中,創(chuàng)建一個(gè)新的 Context對象,此時(shí)只有 temp 變量持有 Context對象,恰好垃圾回收線程在遍歷 main 線程的 虛擬機(jī)棧時(shí), temp變量還為null;
于是在垃圾回收標(biāo)記完成后,發(fā)現(xiàn)Context對象不可達(dá),于是被當(dāng)成垃圾回收了……
如果不暫停業(yè)務(wù)線程,在垃圾回收期間新創(chuàng)建的對象,有可能會被錯誤的回收掉,這真的太可怕了。
之所以可能出現(xiàn)這么離譜的現(xiàn)象, 原因就在于,業(yè)務(wù)線程和垃圾回收線程并行執(zhí)行,誰也無法預(yù)知 垃圾標(biāo)記的工作和 引用關(guān)系的變化 誰先誰后。
只有當(dāng)業(yè)務(wù)線程被暫停,才能保證垃圾回收的標(biāo)記是準(zhǔn)確的
假如業(yè)務(wù)線程被暫停,還會有問題嗎?
第二個(gè)例子中,假設(shè)在 第一行代碼后 Context temp = null; 業(yè)務(wù)線程被暫停。由于 Context對象還未創(chuàng)建,所以不會有對象被回收。
假設(shè)在 第二行代碼執(zhí)行后,被暫停。由于 main線程 temp 變量還持有 Context對象引用,所以 Context對象不會被垃圾回收。
回到第一個(gè)表格的代碼,當(dāng)main線程被暫停后,無論被暫停在 哪一行代碼,垃圾稅收標(biāo)記工作都不會出現(xiàn)任何問題。讀者可以自行假設(shè)論證一下。
總結(jié)
通過 列舉兩個(gè)反例,通過反證法證明 業(yè)務(wù)線程必須被暫停,才可以進(jìn)行垃圾回收標(biāo)記工作。
4年前的面試,我被問到這個(gè)問題時(shí),我的破解思路是,有兩個(gè)業(yè)務(wù)線程,互相修改引用關(guān)系,垃圾回收判斷垃圾對象,會出現(xiàn)錯誤。但是兩個(gè)業(yè)務(wù)線程的場景,實(shí)在復(fù)雜,我無法舉出實(shí)際的例子。其實(shí) 只需要一個(gè)main線程 和垃圾線程對比分析,就能說明問題,根本不需要兩個(gè)業(yè)務(wù)線程來證明這個(gè)問題。
一個(gè)線程尚且出問題,由此可見,業(yè)務(wù)邏輯千奇百怪,當(dāng)存在上千個(gè)業(yè)務(wù)線程時(shí),如果不暫停業(yè)務(wù)線程,就進(jìn)行垃圾回收,該多么可怕!
一般情況下,在聊GC時(shí),沒有面試官會深入到 為什么需要Stop the world 這類問題,但是阿里的面試官獨(dú)辟蹊徑,成功的卷到我。
這個(gè)問題雖然看起來簡單,但真要現(xiàn)場舉例說明,還是有難度的!
我當(dāng)時(shí)沒有回答上來這個(gè)問題,一度以為被掛掉,但最終這輪技術(shù)面試還是通過了…… 也是一個(gè)驚喜