1. 程式人生 > >面試官:你說你熟悉jvm?那你講一下併發的可達性分析

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

這是why技術的第35篇原創文章

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

那天剛剛下完雨,路過這個地方的時候,一瞬間就被這五顏六色的門板和自行車給吸引了,於是拍下了這張圖片。看到這張圖片的時候我就很開心,多鮮活、多舒服的畫面呀。

以後的文章裡面我的第一張配圖都用自己隨時拍下的照片吧。分享生活、分享技術,哈哈。

好了,說迴文章。

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

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

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

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

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

然後你就停下來了嗎?難道你不知道你回答了一句話之後,面試官肯定會接著問你能詳細說明一下嗎?所以,不要停。主動點,面試的時候主動點。你要抓住面試官把話語權交給你的寶貴機會,接著說啊,你得支稜起來

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

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

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

就是不能解決迴圈依賴的問題。

並拿著自己準備的紙和筆快速的畫出下面這樣的圖:

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屆研究生之後的思考》

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

如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

以上。

歡迎關注公眾號【why技術】,堅持輸出原創。分享技術、品味生活,願你我共同進步。

相關推薦

面試:熟悉jvm?一下併發分析

這是why技術的第35篇原創文章 上面這張圖是我還是北漂的時候,在鼓樓附近的衚衕裡面拍的。 那天剛剛下完雨,路過這個地方的時候,一瞬間就被這五顏六色的門板和自行車給吸引了,於是拍下了這張圖片。看到這張圖片的時候我就很開心,多鮮活、多舒服的畫面呀。 以後的文章裡面我的第一張配圖都用自己隨時拍下的照片吧。分享

JVM 中判斷物件是否 “存活” 的演算法 —— 分析演算法

在堆中,幾乎存放著所有的物件例項,那麼回收這些物件例項時,我們需要判斷哪些物件是 “已死” 可以回收的,哪些物件是 “存活” 不需要回收的,下面就來介紹一下 JVM 中如何判斷上述問題的。 基本思路 通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開

JVM----判斷物件是否存活 : 引用計數演算法OR分析演算法?

本篇來自周志明的<<深入理解java虛擬機器>> 在堆裡面存放著Java世界中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些物件之中哪些還“存活”著 ,哪些已經“死去”(即不可能再被任何途徑使用的物件)。 引用計數演算法 很多教科書判斷物

JVM——引用計數演算法與分析演算法

前幾篇部落格我們一起認識了JVM的記憶體模型(程式計數器、虛擬機器棧、本地方法棧、方法區與堆),瞭解了它們的記憶體結構與分配,同時也略帶提到關於記憶體的回收。 JVM——記憶體模型(一):程式計數器 JVM——記憶體模型(二):虛擬機器棧與本地方法棧 JVM——記憶體模型(三):堆與方法

JVM分析演算法-判斷回收的物件

一JVM判斷哪些物件需要回收判斷物件是否需要回收,主要依據是該物件是否被其它地方引用。而判斷該物件是否被其它地方引用,主要有兩種演算法來實現。1、引用計數演算法該演算法的實現原理是:給物件一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用計數器失效時,計數器的值就

深入理解JVM——引用計數法和分析演算法(理解)

引言JVM中的堆和方法區主要用來存放物件(方法區中也儲存了一些靜態變數和全域性變數等資訊),那麼我們要使用GC演算法對其進行回收時首先要考慮的就是該物件是否應該被回收。即判斷該物件是否還有其他的引用或者

jvm原理四:利用分析演算法GC怎麼判斷物件生存還是死亡,經過了幾次過濾,每次都做了什麼

經歷了2次標記過程,即2次過濾過程。第一次:如果物件在進行可達性分析後發現沒有GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方

JVM中垃圾回收機制如何判斷是否死亡?詳解引用計數法和分析

> 因為熱愛,所以堅持。 > 文章下方有本文參考電子書和視訊的**下載地址**哦~ 這節我們主要講垃圾收集的一些基本概念,先了解垃圾收集是什麼、然後觸發條件是什麼、最後虛擬機器如何判斷物件是否死亡。 ### 一、前言   我們都知道Java和C++有一個非常大的區別就是Java有自動的垃圾回收

面試精通 Docker,來詳細說說 Dockerfile 吧

接上一篇:30分鐘快速上手Docker,看這篇就對了! 一、 帶著問題學Dockerfile 1、疑問 我們都知道從遠端倉庫可以pull一個tomcat等映象下來,然後docker run啟動容器,然後docker exec -it 容器id /bin/bash進入容器,往webapps下仍我們的程式。等等這

面試:"我為什麼要聘用"

關於面試,面試官也是人,人的想法可能千奇百怪,雖然其中有一定的規律可以循,但是不乏意料之外的問題。老師的工作是為學生開啟一扇門,讓學生自己走進去,不能使勁把分們拉進來,因為走進來必須是學生自己的事情。講得再多,沒有體悟也是按圖索驥。面試的套路可以說上“兵無常勢,水無常形”能因

阿里面試讓我講講Unicode,我了3秒說沒了,面試真菜

本文首發於微信公眾號:程式設計師喬戈裡 喬哥:首先說說什麼是Unicode、碼點吧~要想搞懂,這些概念必須清楚 什麼是Unicode? 下圖來自http://www.unicode.org/standard/WhatIsUnicode.html中的截圖 Unicode編碼定義了這個世界上幾

對於單例模式面試會怎樣提問呢?又該如何回答呢?

前言 在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題: 為什麼說餓漢式單例天生就是執行緒安全的? 傳統的懶漢式單例為什麼是非執行緒安全的? 怎麼修改傳統的懶漢式單例,使其執行緒變得安全? 執行緒安全的單例的實現還有哪些,怎麼實現? 雙重檢查

面試:淦!0202年還不知道面向物件?

2020年6月13日 多雲轉暴雨⛈️ I'm sad,tired,negative,powerless,miss,lonely fine :) 那你回去等通知吧 面試官:我看你簡歷上說,你的主要程式語言是Java,偶爾也用Python,那麼你可以說一下這兩個的相同點在什麼地方嗎? 山禾:它們都是高階程式

阿里面試:HashMap 熟悉吧?好的,就來聊聊 Redis 字典吧!

最近,小黑哥的一個朋友出去面試,回來跟小黑哥抱怨,面試官不按套路出牌,直接打亂了他的節奏。 事情是這樣的,前面面試問了幾個 Java 的相關問題,我朋友回答還不錯,接下來面試官就問了一句:看來 Java 基礎還不錯,Java HashMap 你熟悉吧? 我朋友回答。工作經常用,有看過原始碼。 我朋友本來

java jvm GC的基石 演算法

在jvm中 任何堆中物件 與GC root set不可達就會被gc回收。 那麼gc root set是什麼呢? 1.虛擬機器棧(棧幀中的本地變量表)中引用的物件。 2.方法區中類靜態屬性引用的物件。 3.方法區中常量引用的物件。 4.本地方法棧中JNI引用的物件。 如果物件

面試:“看簡歷上寫熟悉 Handler 機制,聊聊 IdleHandler 吧?”

一. 序 Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。 本文就來聊聊 H

面試斐波契數列的時候不要高興得太早

  前言 假如面試官讓你編寫求斐波那契數列的程式碼時,是不是心中暗喜?不就是遞迴麼,早就會了。如果真這麼想,那就危險了。 遞迴求斐波那契數列 遞迴,在數學與電腦科學中,是指在函式的定義中使用函式自身的方法。斐波那契數列的計算表示式很簡單: 1F(n) = n;

設計一個getMin功能的棧,如果面試寫一個數組類,一定不要用指標去管理new出來原生的陣列,就是在給自己挖坑

//設計實現一個getMin功能的棧 #include <iostream> #include <stack> using namespace std; class GetMinStack { public:     void push(int x)

如何學好JVM征服面試?一篇Class 類檔案結構還不會嗎?

        Java 跨平臺的基礎 各種不同平臺的虛擬機器與所有平臺都

還在用遞迴實現斐波契數列,面試一定會鄙視到死

       斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368...... &nb