這是why技術的第35篇原創文章java
上面這張圖是我仍是北漂的時候,在鼓樓附近的衚衕裏面拍的。面試
那天剛剛下完雨,路過這個地方的時候,一瞬間就被這五光十色的門板和自行車給吸引了,因而拍下了這張圖片。看到這張圖片的時候我就很開心,多鮮活、多舒服的畫面呀。算法
之後的文章裏面個人第一張配圖都用本身隨時拍下的照片吧。分享生活、分享技術,哈哈。安全
好了,說迴文章。
此次的文章咱們聊聊jvm。jvm能夠說是面試必備技能了。簡歷上寫了,多問幾句。簡歷上沒寫,也得提上幾句。併發
咱們先從一個簡單的熱身題入手,引出本文想要分享的內容。jvm
當面試扯到jvm這一部分的時候,面試官大機率會問你jvm怎麼判斷哪些對象應該回收呢?優化
這種經典的面試題固然難不住你。spa
你會脫口而出引用計數算法和可達性分析算法。線程
而後你就停下來了嗎?難道你不知道你回答了一句話以後,面試官確定會接着問你能詳細說明一下嗎?因此,不要停。主動點,面試的時候主動點。你要抓住面試官把話語權交給你的寶貴機會,接着說啊,你得支棱起來
由於引用計數法的算法是這樣的:在對象中添加一個引用計數器,每當一個地方引用它時,計數器就加一;當引用失效時,計數器值就減一;任什麼時候刻計數器爲零的對象就是不可能再被使用的。3d
可是這樣的算法有個問題,是什麼呢?
不經意間來一波自問自答。讓面試官聽的一愣一愣的。
就是不能解決循環依賴的問題。
並拿着本身準備的紙和筆快速的畫出下面這樣的圖:
Object 1和Object 2其實均可以被回收,可是它們之間還有相互引用,因此它們各自的計數器爲1,則仍是不會被回收。
因此,Java虛擬機沒有采用引用計數法。它採用的是可達性分析算法。
可達性分析算法的思路就是經過一系列的「GC Roots」,也就是根對象做爲起始節點集合,從根節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲引用鏈,若是某個對象到GC Roots間沒有任何引用鏈相連。
用圖論的話來講就是從GC Roots到這個對象不可達時,則證實此對象是不可能再被使用的。因此此對象就是能夠被回收的對象。
說這句話的時候再次,快速的紙上畫出下面的圖:
好了,到這裏就能夠把話語權交給面試官了。由於到這裏,他接下來能夠問的點有不少,你不知道他會問什麼,好比:
你剛剛談到了根節點,那你知道哪些對象能夠做爲根對象嗎?
你剛剛談到了引用,那你知道java裏面有哪幾種引用嗎?
你剛剛談到了可達性分析算法,那若是在該算法中被斷定不可達對象,是否是必定會被回收呢?
談談你熟悉的垃圾回收器和他們的工做過程?
.......
上面的這些問題都太常規了,任何一份面經裏面都會有這樣的幾個問題。
而本文要解決的是下面這個稍微不那麼常見,可是你答題的過程當中必定會提到的點「併發標記」、「浮動垃圾」。
CMS和G1都是有一個併發標記的過程,併發標記要解決什麼問題?帶來了什麼問題?怎麼解決這些問題呢?
剛剛咱們談到的可達性分析算法是須要一個理論上的前提的:該算法的全過程都須要基於一個能保障一致性的快照中才可以分析,這意味着必須全程凍結用戶線程的運行。
爲了避免凍結用戶線程的運行,那咱們就須要讓垃圾回收線程和用戶線程同時運行。
全部咱們來個反證法,先假設不併發標記,即只有垃圾回收線程在運行的流程是怎樣的:
第一步是須要找到根節點,也就是咱們常說的根節點枚舉。
而在這個過程當中,因爲GC Roots是遠遠少於整個java堆中的所有對象的,並且在OopMap此類優化技巧的加持下,它帶來的停頓時間是很是短暫且相對固定的,能夠理解爲不會隨着堆裏面的對象的增長而增長。大概就是下面這個圖的意思:
可是咱們作完根節點枚舉,只是作完了第一步。接下來,咱們須要從GC Roots往下繼續遍歷對象圖,進行"標記"過程。而這一步的停頓時間必然是隨着java堆中的對象增長而增長的。大概就是下面這個圖的意思:
這個邏輯不復雜:堆約大,存儲的對象越多,對象圖結構越複雜,要標記更多對象,因此產生的停頓時間也天然就長了。
全部,通過上面的分析,咱們知道了,根節點的枚舉階段是不太耗時的,也不會隨着java堆裏面存儲的對象增長而增長耗時。而"標記"過程的耗時是會隨着java堆裏面存儲的對象增長而增長的。
"標記"階段是全部使用可達性分析算法的垃圾回收器都有的階段。所以咱們能夠知道,若是可以削減"標記"過程這部分的停頓時間,那麼收益將是可觀的。
因此併發標記要解決什麼問題呢?
就是要消減這一部分的停頓時間。那就是讓垃圾回收器和用戶線程同時運行,併發工做。也就是咱們說的併發標記的階段。
在說帶來什麼問題以前,咱們必須得先搞清楚一個問題:
爲何遍歷對象圖的時候必須在一個能保障一致性的快照中?
爲了說明這個問題,咱們就要引入"三色標記"大法了。注意:"三色標記"也是jvm的一個考點哦。
什麼是"三色標記"?《深刻理解Java虛擬機(第三版)》中是這樣描述的:
在遍歷對象圖的過程當中,把訪問都的對象按照"是否訪問過"這個條件標記成如下三種顏色:
白色:表示對象還沒有被垃圾回收器訪問過。顯然,在可達性分析剛剛開始的階段,全部的對象都是白色的,若在分析結束的階段,仍然是白色的對象,即表明不可達。
黑色:表示對象已經被垃圾回收器訪問過,且這個對象的全部引用都已經掃描過。黑色的對象表明已經掃描過,它是安全存活的,若是有其它的對象引用指向了黑色對象,無須從新掃描一遍。黑色對象不可能直接(不通過灰色對象)指向某個白色對象。
灰色:表示對象已經被垃圾回收器訪問過,但這個對象至少存在一個引用尚未被掃描過。
讀完上面描述,再品一品下面的圖:
能夠看到,灰色對象是黑色對象與白色對象之間的中間態。當標記過程結束後,只會有黑色和白色的對象,而白色的對象就是須要被回收的對象。
在可達性分析的掃描過程當中,若是隻有垃圾回收線程在工做,那確定不會有任何問題。
可是垃圾回收器和用戶線程同時運行呢?這個時候就有點意思了。
垃圾回收器在對象圖上面標記顏色,而同時用戶線程在修改引用關係,引用關係修改了,那麼對象圖就變化了,這樣就有可能出現兩種後果:
一種是把本來消亡的對象錯誤的標記爲存活,這不是好事,可是實際上是能夠容忍的,只不過產生了一點逃過本次回收的浮動垃圾而已,下次清理就能夠。
一種是把本來存活的對象錯誤的標記爲已消亡,這就是很是嚴重的後果了,一個程序還須要使用的對象被回收了,那程序確定會所以發生錯誤。
當面試官問你:爲何會產生浮動垃圾的時候,你就能夠用上面的話來回答。
可是大機率狀況下面試官應該更加關心第二種狀況。
他可能會問:你剛剛說的第二種狀況,"把本來存活的對象錯誤的標記爲已消亡"能具體的說明一下嗎?怎麼消亡的?垃圾回收器是怎麼解決這個問題的?
因此接下來,咱們主要分析一下併發標記的過程當中"對象消失"的問題。具體"對象"是怎麼沒了的。
這裏藉助《深刻理解Java虛擬機(第三版)》的示例,可是第三版的示例的描述寫的不是特別容易理解,我就盡我所能的描述的清楚一些,下面會結合動圖,分析標記的三種狀況:
咱們先看一下一次正常的標記過程:
首先是初始狀態,很簡單,只有GC Roots是黑色的。同時須要注意下面的圖片的箭頭方向,表明的是有向的,好比其中的一條引用鏈是:
根節點->5->6->7->8->11->10
在掃描的過程當中,變化是這樣的:
心裏OS:爲了作下面的這些動圖、爲了把動圖裏面的每張圖截的大小一個像素都不差,鬼知道我作的多辛苦,作瞎個人鈦合金狗眼。
你看上面的動圖,灰色對象始終是介於黑色和白色之間的。當掃描順利完成後,對象圖就變成了這個樣子:
此時,黑色對象是存活的對象,白色對象是消亡了,能夠回收的對象。
記住,上面演示的是一切都是那麼美好的正常狀況。
接下來,咱們看看對象消失的狀況:
若是用戶線程在標記的時候,修改了引用關係,就會出現下面的狀況:
當掃描完成後,對象圖就變成了這個樣子:
這時,咱們和以前分析的正常掃描結束的對象圖對比,就能清楚的看到,掃描完成後,本來還在被對象5引用的對象9,因爲是白色對象,因此根據三色標記原則,對象9會被當成垃圾回收。
這樣就出現了對象消失的狀況。
下面再給各位看看另一種"對象消失"的現象:
上面演示的是用戶線程切斷引用後從新被黑色對象引用的對象就是原來引用鏈的一部分。
對象7和對象10原本就是原引用鏈(根節點->5->6->7->8->11->10)的一部分。修改後的引用鏈變成了(根節點->5->6->7->10)。
當掃描完成後,對象圖就變成了這個樣子:
因爲黑色對象不會從新掃描,這將致使掃描結束後對象10和對象11都會回收了。他們都是被修改以前的原來的引用鏈的一部分。
因此,回到最開始的疑問:併發標記帶來了什麼問題?
通過咱們上面三種狀況(一種正常狀況,兩種"對象丟失"的狀況)的動圖分析,和掃描完成後的最終對象圖進行分析對比,咱們知道了,併發標記除了會產生浮動垃圾,還會出現"對象消失"的問題。
有一個大佬叫Wilson,他在1994年在理論上證實了,當且僅當如下兩個條件同時知足時,會產生"對象消失"的問題,原來應該是黑色的對象被誤標爲了白色:
條件一:賦值器插入了一條或者多條從黑色對象到白色對象的新引用。
條件二:賦值器刪除了所有從灰色對象到該白色對象的直接或間接引用。
你在結合咱們上面出現過的圖捋一捋上面的這兩個條件,是否是當且僅當的關係:
黑色對象5到白色對象9之間的引用是新建的,對應條件一。
黑色對象6到白色對象9之間的引用被刪除了,對應條件二。
因爲兩個條件之間是當且僅當的關係。因此,咱們要解決併發標記時對象消失的問題,只須要破壞兩個條件中的任意一個就行。
因而產生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。
在HotSpot虛擬機中,CMS是基於增量更新來作併發標記的,G1則採用的是原始快照的方式。
什麼是增量更新呢?
增量更新要破壞的是第一個條件(賦值器插入了一條或者多條從黑色對象到白色對象的新引用),當黑色對象插入新的指向白色對象的引用關係時,就將這個新插入的引用記錄下來,等併發掃描結束以後,再將這些記錄過的引用關係中的黑色對象爲根,從新掃描一次。
能夠簡化的理解爲:黑色對象一旦插入了指向白色對象的引用以後,它就變回了灰色對象。
下面的圖就是一次併發掃描結束以後,記錄了黑色對象5新指向了白色對象9:
這樣對象9又被掃描成爲了黑色。也就不會被回收,因此不會出現對象消失的狀況。
什麼是原始快照呢?
原始快照要破壞的是第二個條件(賦值器刪除了所有從灰色對象到該白色對象的直接或間接引用),當灰色對象要刪除指向白色對象的引用關係時,就將這個要刪除的引用記錄下來,在併發掃描結束以後,再將這些記錄過的引用關係中的灰色對象爲根,從新掃描一次。
這個能夠簡化理解爲:不管引用關係刪除與否,都會按照剛剛開始掃描那一刻的對象圖快照開進行搜索。
須要注意的是,上面的介紹中不管是對引用關係記錄的插入仍是刪除,虛擬機的記錄操做都是經過寫屏障實現的。寫屏障也是一個重要的知識點,可是不是本文重點,就不進行詳細介紹了。
只是補充兩點:
1.這裏的寫屏障和咱們常說的爲了解決併發亂序執行問題的"內存屏障"不是一碼事,須要區分開來。
2.寫屏障能夠看做虛擬機層面對"引用類型字段賦值"這個動做的AOP切面,在引用對象賦值時會產生一個環形通知,供程序執行額外的動做,也就是說賦值的先後都在寫屏障的覆蓋範疇內。在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值後的則叫做寫後屏障(Post-Write Barrier)。
因此,通過簡單的推導咱們能夠知道:
增量更新用的是寫後屏障(Post-Write Barrier),記錄了全部新增的引用關係。
原始快照用的是寫前屏障(Pre-Write Barrier),將全部即將被刪除的引用關係的舊引用記錄下來。
最近有不少讀者在找我修改簡歷、諮詢工做的相關事情了,我就知道立刻又要開始春招了。
其實我也不是頗有資格給大家修改簡歷,也不是一個技術很牛逼的人,只是把我知道的分享出來了而已,不只能讓我鞏固知識,仍是倒逼我進行知識輸入,在此以外還能對你有一點點幫助,那就是我文章的所有價值所在。
另外若是你正在經歷春招或者社招,有興趣的能夠閱讀一下我以前的這篇文章,看看是否有一點點幫助:
《面試了15位來自985/211高校的2020屆研究生以後的思考》
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
以上。