天天看點

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

上面這張圖是我還是北漂的時候,在鼓樓附近的胡同裡面拍的。

這次的文章我們聊聊jvm。jvm可以說是面試必備技能了。履歷上寫了,多問幾句。履歷上沒寫,也得提上幾句。

我們先從一個簡單的熱身題入手,引出本文想要分享的内容。

當面試扯到jvm這一部分的時候,面試官大機率會問你jvm怎麼判斷哪些對象應該回收呢?

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

這種經典的面試題當然難不住你。

你會脫口而出引用計數算法和可達性分析算法。

然後你就停下來了嗎?難道你不知道你回答了一句話之後,面試官肯定會接着問你能詳細說明一下嗎?

是以,不要停。主動點,面試的時候主動點。你要抓住面試官把話語權交給你的寶貴機會,接着說啊,你得支棱起來:

因為引用計數法的算法是這樣的:在對象中添加一個引用計數器,每當一個地方引用它時,計數器就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。

但是這樣的算法有個問題,是什麼呢?

不經意間來一波自問自答。讓面試官聽的一愣一愣的。

就是不能解決循環依賴的問題。

并拿着自己準備的紙和筆快速的畫出下面這樣的圖:
面試官:你說你熟悉jvm?那你講一下并發的可達性分析

Object 1和Object 2其實都可以被回收,但是它們之間還有互相引用,是以它們各自的計數器為1,則還是不會被回收。

是以,Java虛拟機沒有采用引用計數法。它采用的是可達性分析算法。

可達性分析算法的思路就是通過一系列的“GC Roots”,也就是根對象作為起始節點集合,從根節點開始,根據引用關系向下搜尋,搜尋過程所走過的路徑稱為引用鍊,如果某個對象到GC Roots間沒有任何引用鍊相連。

用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。是以此對象就是可以被回收的對象。

說這句話的時候再次,快速的紙上畫出下面的圖:
面試官:你說你熟悉jvm?那你講一下并發的可達性分析

好了,到這裡就可以把話語權交給面試官了。因為到這裡,他接下來可以問的點有很多,你不知道他會問什麼,比如:

你剛剛談到了根節點,那你知道哪些對象可以作為根對象嗎?

你剛剛談到了引用,那你知道java裡面有哪幾種引用嗎?

你剛剛談到了可達性分析算法,那如果在該算法中被判定不可達對象,是不是一定會被回收呢?

談談你熟悉的垃圾回收器和他們的工作過程?

.......

上面的這些問題都太正常了,任何一份面經裡面都會有這樣的幾個問題。

而本文要解決的是下面這個稍微不那麼常見,但是你答題的過程中一定會提到的點“并發标記”、“浮動垃圾”。

CMS和G1都是有一個并發标記的過程,并發标記要解決什麼問題?帶來了什麼問題?怎麼解決這些問題呢?

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

并發标記要解決什麼問題?

剛剛我們談到的可達性分析算法是需要一個理論上的前提的:該算法的全過程都需要基于一個能保障一緻性的快照中才能夠分析,這意味着必須全程當機使用者線程的運作。

為了不當機使用者線程的運作,那我們就需要讓垃圾回收線程和使用者線程同時運作。

所有我們來個反證法,先假設不并發标記,即隻有垃圾回收線程在運作的流程是怎樣的:

第一步是需要找到根節點,也就是我們常說的根節點枚舉。

而在這個過程中,由于GC Roots是遠遠少于整個java堆中的全部對象的,而且在OopMap此類優化技巧的加持下,它帶來的停頓時間是非常短暫且相對固定的,可以了解為不會随着堆裡面的對象的增加而增加。大概就是下面這個圖的意思:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

但是我們做完根節點枚舉,隻是做完了第一步。接下來,我們需要從GC Roots往下繼續周遊對象圖,進行"标記"過程。而這一步的停頓時間必然是随着java堆中的對象增加而增加的。大概就是下面這個圖的意思:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

這個邏輯不複雜:堆約大,存儲的對象越多,對象圖結構越複雜,要标記更多對象,是以産生的停頓時間也自然就長了。

所有,經過上面的分析,我們知道了,根節點的枚舉階段是不太耗時的,也不會随着java堆裡面存儲的對象增加而增加耗時。而"标記"過程的耗時是會随着java堆裡面存儲的對象增加而增加的。

"标記"階段是所有使用可達性分析算法的垃圾回收器都有的階段。是以我們可以知道,如果能夠削減"标記"過程這部分的停頓時間,那麼收益将是可觀的。

是以并發标記要解決什麼問題呢?

就是要消減這一部分的停頓時間。那就是讓垃圾回收器和使用者線程同時運作,并發工作。也就是我們說的并發标記的階段。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

并發标記帶來了什麼問題?

在說帶來什麼問題之前,我們必須得先搞清楚一個問題:

為什麼周遊對象圖的時候必須在一個能保障一緻性的快照中?

為了說明這個問題,我們就要引入"三色标記"大法了。注意:"三色标記"也是jvm的一個考點哦。

什麼是"三色标記"?

在周遊對象圖的過程中,把通路都的對象按照"是否通路過"這個條件标記成以下三種顔色:

白色:表示對象尚未被垃圾回收器通路過。顯然,在可達性分析剛剛開始的階段,所有的對象都是白色的,若在分析結束的階段,仍然是白色的對象,即代表不可達。

黑色:表示對象已經被垃圾回收器通路過,且這個對象的所有引用都已經掃描過。黑色的對象代表已經掃描過,它是安全存活的,如果有其它的對象引用指向了黑色對象,無須重新掃描一遍。黑色對象不可能直接(不經過灰色對象)指向某個白色對象。

灰色:表示對象已經被垃圾回收器通路過,但這個對象至少存在一個引用還沒有被掃描過。

讀完上面描述,再品一品下面的圖:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

可以看到,灰色對象是黑色對象與白色對象之間的中間态。當标記過程結束後,隻會有黑色和白色的對象,而白色的對象就是需要被回收的對象。

在可達性分析的掃描過程中,如果隻有垃圾回收線程在工作,那肯定不會有任何問題。

但是垃圾回收器和使用者線程同時運作呢?這個時候就有點意思了。

垃圾回收器在對象圖上面标記顔色,而同時使用者線程在修改引用關系,引用關系修改了,那麼對象圖就變化了,這樣就有可能出現兩種後果:

一種是把原本消亡的對象錯誤的标記為存活,這不是好事,但是其實是可以容忍的,隻不過産生了一點逃過本次回收的浮動垃圾而已,下次清理就可以。

一種是把原本存活的對象錯誤的标記為已消亡,這就是非常嚴重的後果了,一個程式還需要使用的對象被回收了,那程式肯定會是以發生錯誤。

當面試官問你:為什麼會産生浮動垃圾的時候,你就可以用上面的話來回答。

但是大機率情況下面試官應該更加關心第二種情況。

他可能會問:你剛剛說的第二種情況,"把原本存活的對象錯誤的标記為已消亡"能具體的說明一下嗎?怎麼消亡的?垃圾回收器是怎麼解決這個問題的?

是以接下來,我們主要分析一下并發标記的過程中"對象消失"的問題。具體"對象"是怎麼沒了的。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

這裡借助《深入了解Java虛拟機(第三版)》的示例,但是第三版的示例的描述寫的不是特别容易了解,我就盡我所能的描述的清楚一些,下面會結合動圖,分析标記的三種情況:

正常标記

我們先看一下一次正常的标記過程:

首先是初始狀态,很簡單,隻有GC Roots是黑色的。同時需要注意下面的圖檔的箭頭方向,代表的是有向的,比如其中的一條引用鍊是:

根節點->5->6->7->8->11->10

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

在掃描的過程中,變化是這樣的:

内心OS:為了做下面的這些動圖、為了把動圖裡面的每張圖截的大小一個像素都不差,鬼知道我做的多辛苦,做瞎我的钛合金狗眼。
面試官:你說你熟悉jvm?那你講一下并發的可達性分析

你看上面的動圖,灰色對象始終是介于黑色和白色之間的。當掃描順利完成後,對象圖就變成了這個樣子:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

此時,黑色對象是存活的對象,白色對象是消亡了,可以回收的對象。

記住,上面示範的是一切都是那麼美好的正常情況。

對象消失的情況一

接下來,我們看看對象消失的情況:

如果使用者線程在标記的時候,修改了引用關系,就會出現下面的情況:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

當掃描完成後,對象圖就變成了這個樣子:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

這時,我們和之前分析的正常掃描結束的對象圖對比,就能清楚的看到,掃描完成後,原本還在被對象5引用的對象9,由于是白色對象,是以根據三色标記原則,對象9會被當成垃圾回收。

這樣就出現了對象消失的情況。

對象消息的情況二

下面再給各位看看另外一種"對象消失"的現象:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

上面示範的是使用者線程切斷引用後重新被黑色對象引用的對象就是原來引用鍊的一部分。

對象7和對象10本來就是原引用鍊(根節點->5->6->7->8->11->10)的一部分。修改後的引用鍊變成了(根節點->5->6->7->10)。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

由于黑色對象不會重新掃描,這将導緻掃描結束後對象10和對象11都會回收了。他們都是被修改之前的原來的引用鍊的一部分。

是以,回到最開始的疑問:并發标記帶來了什麼問題?

經過我們上面三種情況(一種正常情況,兩種"對象丢失"的情況)的動圖分析,和掃描完成後的最終對象圖進行分析對比,我們知道了,并發标記除了會産生浮動垃圾,還會出現"對象消失"的問題。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

怎麼解決"對象消失"問題呢?

有一個大佬叫Wilson,他在1994年在理論上證明了,當且僅當以下兩個條件同時滿足時,會産生"對象消失"的問題,原來應該是黑色的對象被誤标為了白色:

條件一:指派器插入了一條或者多條從黑色對象到白色對象的新引用。

條件二:指派器删除了全部從灰色對象到該白色對象的直接或間接引用。

你在結合我們上面出現過的圖捋一捋上面的這兩個條件,是不是當且僅當的關系:

黑色對象5到白色對象9之間的引用是建立的,對應條件一。

黑色對象6到白色對象9之間的引用被删除了,對應條件二。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

由于兩個條件之間是當且僅當的關系。是以,我們要解決并發标記時對象消失的問題,隻需要破壞兩個條件中的任意一個就行。

于是産生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。

在HotSpot虛拟機中,CMS是基于增量更新來做并發标記的,G1則采用的是原始快照的方式。

什麼是增量更新呢?

增量更新要破壞的是第一個條件(指派器插入了一條或者多條從黑色對象到白色對象的新引用),當黑色對象插入新的指向白色對象的引用關系時,就将這個新插入的引用記錄下來,等并發掃描結束之後,再将這些記錄過的引用關系中的黑色對象為根,重新掃描一次。

可以簡化的了解為:黑色對象一旦插入了指向白色對象的引用之後,它就變回了灰色對象。

下面的圖就是一次并發掃描結束之後,記錄了黑色對象5新指向了白色對象9:

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

這樣對象9又被掃描成為了黑色。也就不會被回收,是以不會出現對象消失的情況。

什麼是原始快照呢?

原始快照要破壞的是第二個條件(指派器删除了全部從灰色對象到該白色對象的直接或間接引用),當灰色對象要删除指向白色對象的引用關系時,就将這個要删除的引用記錄下來,在并發掃描結束之後,再将這些記錄過的引用關系中的灰色對象為根,重新掃描一次。

這個可以簡化了解為:無論引用關系删除與否,都會按照剛剛開始掃描那一刻的對象圖快照開進行搜尋。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

需要注意的是,上面的介紹中無論是對引用關系記錄的插入還是删除,虛拟機的記錄操作都是通過寫屏障實作的。寫屏障也是一個重要的知識點,但是不是本文重點,就不進行詳細介紹了。

隻是補充兩點:

1.這裡的寫屏障和我們常說的為了解決并發亂序執行問題的"記憶體屏障"不是一碼事,需要區分開來。

2.寫屏障可以看作虛拟機層面對"引用類型字段指派"這個動作的AOP切面,在引用對象指派時會産生一個環形通知,供程式執行額外的動作,也就是說指派的前後都在寫屏障的覆寫範疇内。在指派前的部分的寫屏障叫做寫前屏障(Pre-Write Barrier),在指派後的則叫作寫後屏障(Post-Write Barrier)。

是以,經過簡單的推導我們可以知道:

增量更新用的是寫後屏障(Post-Write Barrier),記錄了所有新增的引用關系。

原始快照用的是寫前屏障(Pre-Write Barrier),将所有即将被删除的引用關系的舊引用記錄下來。

面試官:你說你熟悉jvm?那你講一下并發的可達性分析

最後說一句

1.建議先看第3版這部分後再看本文。2.最近有很多讀者在找我修改履歷、咨詢工作的相關事情了,我就知道馬上又要開始春招了。

其實我也不是很有資格給你們修改履歷,也不是一個技術很牛逼的人,隻是把我知道的分享出來了而已,不僅能讓我鞏固知識,還是倒逼我進行知識輸入,在此之外還能對你有一點點幫助,那就是我文章的全部價值所在。

才疏學淺,難免會有纰漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。