面試官:G1回收器怎麼知道你是什麼時候的垃圾?

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

上面的圖片是我上週末在家拍的。以後的文章裡面我的第一張配圖都用自己隨手拍下的照片吧。分享生活,分享技術,哈哈。

陽臺上的花開了,成都的春天快來了,疫情也應該快要過去了吧。

最近在看《霍亂時期的愛情》,不知道為什麼和《大話西遊》聯繫了起來,所以你可以看到玻璃上的倒影,是我在看《大話西遊》。

誰都曾經有過大鬧天宮的夢想,愛上層樓的憂愁,但是早晚有一天,你也會像他轉身之後一樣,走在路上,像一條狗。

好了,說迴文章

面試官:G1回收器怎麼知道你是什麼時候的垃圾?


讓你看看“浮動垃圾”

上週《面試官:你說你熟悉jvm?那你講一下併發的可達性分析》這篇文章主要聊了jvm的可達性分析算法。

藉助“三色標記”大法分析了垃圾回收線程掃描的過程中,用戶線程同時執行修改引用關係的操作時,可能會出現的“對象消失”問題,以及其對應的兩種解決方案

增量更新和原始快照。

在文章中我寫道:對象關係圖的變化會導致出現兩種情況一是“浮動垃圾”,二是“對象消失”。大概率的情況下面試官更加關心第二種情況,因為第二種情況會給程序帶來異常。接下來我就做動圖分析了“對象消失”的情況

但是我是萬萬沒想到呀,讀者更關心的是“浮動垃圾”。有的讀者就來問我,浮動垃圾是怎麼產生的,你倒是給個圖啊。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

像我這樣的又暖又有料的硬核原創作者,你說你要,那我肯定是要給你的。

下面就給你補上“浮動垃圾”的動圖:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

當併發標記完成後,對象圖就變成了下面這個樣子:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

你看出來了吧。對象7,8,4,11,10都是浮動垃圾。因為他們被標記成了黑色,所以逃過了本次垃圾回收。

什麼?你問我為什麼黑色就不回收了?你個假粉絲,建議你先去讀一讀上週的文章。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

G1垃圾回收時新對象怎麼處理?

有的讀者就提出了另外的很有探討性的問題:

why哥你好,你《面試官:你說你熟悉jvm?那你講一下併發的可達性分析》這篇文章主要解決了在併發標記階段,GC線程和用戶線程併發執行時,用戶線程修改了對象引用關係,導致“對象消失”的問題。G1是採用原始快照加寫前屏障的方式解決這個問題的。

但是我還有另外的一個問題:用戶線程執行時不僅修改了對象引用關係,還新分配了新對象,我覺得這個情況是非常常見的,G1是如何找到並處理這些對象的呢?

換句話說,就是文章標題啦:G1收集器是怎麼知道這些對象是什麼時候應該進行垃圾標記的?

這是一個好問題,一看就是用心讀了文章並帶有自己的思考。很不錯。

這位讀者的問題屬於第一個問題的連環炮,讓我突然有了一種掉進了面試官布好的天羅地網裡面的感覺。

面試官先故意漏出破綻,讓你聊“對象消失”、“三色標記”、“增量更新”。然後等你得意洋洋的時候,突然拋出第二個問題:

剛剛對象消失的問題回答的不錯,那如果併發標記的時候用戶線程分配了新對象,G1是怎麼處理的呢?

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

說實話,我覺得只要你簡歷上沒有寫精通jvm,面試一般問到這種程度的我覺得是真的到了探討的地步了。答的上來加分,答不上來也不扣分。

遙想2016年,我剛畢業,隻身闖北京的時候,一連面試了9家公司,沒有一家公司聊到 jvm (當然我當時面的是初級開發)。現在不一樣了,不知道什麼時候 jvm 從進階面試題,變成了初級面試題。面試階段如果沒有問 jvm ,就感覺不是一次完整的面試。

我覺得就這幾年面試題的變化,其實也就是反映了一個現象:想入行的人越來越多,導致入行的門檻越來越高。

不是jvm的地位變了,而是門檻越來越高了。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

好了,瞎逼逼完了,接下來我們聊聊G1。

初識Garbage First(G1)

我不知道你是怎麼知道G1的,但是我是從周志明大大的《深入理解Java虛擬機(第2版)》這本書裡面第一次知道G1收集器的。

我記得當時讀到G1的時候感覺這就是天書啊。

因為作者在介紹G1之前介紹了很多其他的收集器,我先給你看一下目錄,帶你回顧回顧:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

可以看到,3.5.1節到3.5.6節介紹的收集器工作的時候, Java 堆的內存佈局是按照新生代,老年代進行整體的區域劃分的。

但是到了G1收集器, Java 堆的內存佈局就有點"妖豔賤貨"了。然後就有點越來越看不懂了,當時的場景就像下面這樣:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

它雖然還是保留的有新生代和老年代的概念,但是新生代和老年代之前再也不是區域上的隔離了。它將整個 Java 堆劃分為多個大小相等的獨立區域,叫做 Region 。而新生代和老年代就是由一個個 Region 動態組成的區域,它們可以是不連續的區間。

每一個 Region 都可以根據需要,扮演新生代的 Eden 空間,Survivor 空間,或者老年代空間。除此之外它還有一類特殊的區域叫做 Humongous,專門用來存儲大對象。

上面說的是啥意思呢?其實用圖片看起來就非常直觀了:

比如對於 CMS,使用的堆內存結構如下:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

可以看到上面的圖片中不論是年輕代、老年代都是邏輯上連續的空間(但是不要求物理上的連續)。

而G1的堆內存被劃分為多個大小相等的 Region ,但是 Region 的總個數在 2048 個左右,默認是 2048 。對於一個 Region 來說,是邏輯連續的一段空間,其大小的取值範圍是 1MB 到 32MB 之間。

結構如下:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

圖片來源-文末資料4

上面的E、S和沒有寫字母的藍色方塊(可以理解為old)沒啥說的。

但是可以看到H是以往的垃圾收集器中沒有的概念,它代表 Humongous,這表示這些 Region 存儲的是巨型對象(humongous object,H-obj),當新建對象大小超過 Region 大小一半時,直接在新的一個或多個連續 Region 中分配,並標記為H。

說實話上面的這概念已經“爛大街”了,任何一篇寫G1都會聊到,包括本文也是。


沒辦法啊,朋友們,這是引子,必須得先聊幾句。就像鬥地主,你第一手牌能直接出王炸嗎?不能啊,你不得先來一個對三,循序漸進啊。


下面我送你一個小彩蛋吧。

注意到我上面說的幾個數據了嗎,2048個左右,1MB到32MB,這些數據是哪裡來的呢,我說你就信了嗎?

很多文章聊到G1的時候都只是說堆內存被劃分為多個大小相等的 Region

, Region 大小的取值範圍為 1MB 到 32MB ,但是並沒有提到 2048 這回事,我來給你尋根問祖一下:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

我找到的第一個數據來源於上面的這篇論文,即文末的資料4:

The goal is to have around 2048 regions for the total heap.

這篇論文的作者是Monica Beckwith,你可以去搜一下,她(是的,我沒打錯,是個妹子)擔任過Oracle G1 垃圾收集器性能團隊 Leader,權威吧。

第二個數據來源當然是源碼了,更權威吧:

http://hg.openjdk.java.net/jdk/jdk/file/fa2f93f99dbc/src/hotspot/share/gc/g1/heapRegionBounds.hpp

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

知道這個2048重要嗎?我覺得不重要。

但是知道了就更牛逼呀!當妹子聊到2048的時候她只知道這是一個遊戲,你要告訴她這個數字也是G1的Region的默認個數。

事了拂衣去,深藏功與名。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

G1的工作步驟

這一部分,也是耳熟能詳的部分,但是忍一忍,馬上就要到你高呼:臥槽,牛逼的部分了。

眾所周知,一般我們說G1的收集過程分為下面這四個步驟(下面四個步驟的描述來自於《深入理解Java虛擬機(第3版)》):

說實在的,下面的描述確實看的讓人很懵逼的。面試的過程中問到這一部分的時候,我相信大多數朋友都是硬背下來的。


所以,本文的目的就是為了讓你理解下面這幾個階段的具體過程。


這麼說吧,如果看完這篇文章你還是沒搞懂上面這幾個階段的話,那你再讀一遍。


再讀一遍,還是沒懂的話,那我這篇文章就算寫失敗了。

初始標記(Initial Marking):這階段僅僅只是標記GC Roots能直接關聯到的對象並修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確的可用的Region中創建新對象,這階段需要停頓線程,但是耗時很短。

而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。

併發標記(Concurrent Marking)

:從GC Roots開始對堆的對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出存活的對象,這階段耗時較長,但是可以與用戶程序併發執行。

當對象圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的對象。

最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的 SATB 記錄。

篩選回收(Live Data Counting and Evacuation):負責更新 Region 的統計數據,對各個 Region 的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃。

可以自由選擇任意多個 Region 構成回收集,然後把決定回收的那一部分 Region 的存活對象複製到空的 Region 中,再清理掉整個舊 Region 的全部空間。

這裡的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成的。

上面雖然有4個階段,但是從上帝視角,我們可以把它分為兩大部分,或者說從整個算法的角度,我們可以切分為兩大部分:

1.Global Concurrent Marking:全局併發標記。

2.Evacuation Pauses:該階段是負責把一部分Region裡的活對象拷貝到空Region裡面去,然後回收原本的Region空間。

為什麼我敢這樣去劃分呢?

一部分原因來自這篇論文中:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

《Garbage-First Garbage Collection》這篇論文是 sun 實驗室在 2004 年發佈的第一篇關於 G1 的論文。夠權威吧?

該論文中,2.3小節就是介紹 Evacuation Pauses ,2.5小節就是介紹 Concurrent Marking ,下面是部分內容截圖:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

另一部分原因是 R大 也這樣說的(見文末參考資料)。

接下來,要回答讀者提出的問題,我們就需要了解全局併發標記階段。

全局併發標記

這一節就是回答這個問題:用戶線程執行的時候不僅修改了對象引用關係,還新分配了新對象,G1 是如何找到並處理這些對象的呢?

要回答這個問題,就涉及到 TAMS 了。前面我引用的書裡說:

初始標記(Initial Marking):這階段僅僅只是標記 GC Roots 能直接關聯到的對象並修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確的可用的 Region 中創建新對象。

這句話,每個字都能看懂,連在一起讀,也品出點兒味道,但是總感覺似懂非懂的樣子。

什麼是 TAMS?什麼是正確可用的 Region?新對象是創建在 Region 中的哪個位置的?

我們先從論文入手,我撿關鍵點給你說:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

1.有兩個 bitmap。

2.一個叫 previous,一個叫 next。

3.previous bitmap 是 concurrent marking 階段完成後的最後一個 bitmap。(有點繞,後面會解釋)。

4.next bitmap 是當前將要或正在進行的 concurrent marking 的結果。

5.當標記完成後,兩個 bitmap 會交換角色。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

1.標記週期的第一個階段就是清理 next bitmap。

2.然後,初始標記階段 Stop The World(後面簡稱STW),目的是標記 GC Roots 能直接關聯到的對象。該階段藉助 Minor GC 完成,沒有額外的停頓。

3.每個 Region 包含兩個 TAMS。

4.一個對應前一輪標記,一個對應下一次標記。

從論文中我們可以知道,G1的Concurrent Marking 用了兩個 marking bitmap。

一個 previous Bitmap 記錄的是上一輪 Concurrent Marking 後的對象標記狀態,因為上一輪已經完成,所以這個bitmap的信息可以直接使用。

一個 next Bitmap 記錄的是當前這一輪 Concurrent Marking 的結果。這個bitmap是當前將要或正在進行的 Concurrent Marking 的結果,尚未完成,所以還不能使用。

我們可以假設一次併發標記變成後的 Bitmap(previous Bitmap) 大概長這樣:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

白色地址之間是可以回收的對象,灰色地址之間是不可以回收的對象。

除了兩個 bitmap 外,還有兩個 TAMS(top at mark start)。每個 Region 都有兩個 TAMS,分別是 previous TAMS 和 next TAMS。

bitmap 和 TAMS 可以用下面的圖片來表示:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

首先我們可以看到 bottom 和 top 之間是一個 Region 已使用的部分。Top 到 end 之前是一個 Region 未使用的部分。

另外可以看到上面我留了四個問號,接下我們的目的就是填補這些問號。當這些問號被填上之後,所有的問題都會迎刃而解。

兩個 Bitmap 和兩個 TAMS 是怎麼工作的呢?

接下來按照:

初始標記(Initial Marking)

併發標記(Concurrent Marking)

最終標記(final marking,也叫Remark)

清理階段(Cleanup)

這四個階段作圖說明

初始標記(Initial Marking)

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

從圖片可以看到初始標記階段 nextBitmap 是清空狀態,沒有標記任何存活的對象。

接著我們再次回到書中的描述裡,我給你逐字描述清楚:

初始標記(Initial Marking):這階段僅僅只是標記 GC Roots 能直接關聯到的對象並修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確的可用的 Region 中創建新對象。

GC Roots 能直接關聯到的對象:就是一個 Region 已經使用過的部分,所以在 Bottom 與 top 之間。

修改 TAMS 的值:就是讓此時的 prevTAMS 指向 Bottom ,也就是一個 Region 內存地址起始值。讓此時的 nextTAMS 指向 Top。Top 實際上就是一個 Region 未分配區域和已分配區域的分界點。

正確的可用的 Region :對一個 Region 來說,當上面的 nextBitmap 為空、4個指針都準備就緒後,這個 Region 在下一階段用戶程序併發運行時,就是一個正確的 Region。

下一階段用戶程序併發運行時,在正確的可用的 Region 中創建新對象是什麼意思呢?

下一階段用戶程序併發運行時指的就是併發標記階段。

併發標記(Concurrent Marking)

先看前面引用的書中描述:

併發標記(Concurrent Marking):從 GC Roots 開始對堆的對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出存活的對象,這階段耗時較長,但是可以與用戶程序併發執行。當對象圖掃描完成以後,還要重新處理 SATB 記錄下的在併發時有引用變動的對象。

再看動圖:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

從 GC Roots 開始對堆的對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出存活的對象:

意思就是說在併發標記階段, GC 線程工作在 prevTAMS 和 NextTAMS 之間,對堆裡的對象進行可達性分析(回想一下“三色標記”),標記完成後, NextBitmap 就有對應有值了(裡面放的是地址值),黑色對應的是存活對象,白色對應的垃圾對象。

這樣就找出存活對象了。

但是書中並沒有提及用戶線程分配對象的情況。所以讀者提出的問題,在書中也找不到明確的答案。

答案就是: NextTAMS 與 Top 之間的對象,就是本次併發標記階段用戶線程新分配的對象,它們是隱式存活的。

為什麼這麼說?你去品一品論文裡面我框起來的這句話。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

但是面試官想要的是這一句話的答案嗎?不是的。

你聽到這個問題後,你先微微一皺眉,做出沉思狀,然後輕輕說說一句:這個問題問的很好,我先組織一下語言。(先舔他一波)

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

然後你按照階段把圖畫出來,指著給他講 TAMS 和 Bitmap 是怎麼工作的。

另外,關於 NextTAMS 與 Top 為什麼是重疊的,也得補充說明一下:併發標記的前一個階段是初始標記。由於初始標記是 STW 的,所以從動圖中我們可以看到:併發標記開始,即初始標記結束的時候, NextTAMS 與 Top 是重疊的。

隨著併發標記過程的進行, NextBitmap 被填充上了值。而 NextTAMS 與 Top 之間的區域越來越大,這就是用戶線程在併發標記階段分配的新對象。

同時通過下面的圖我們可以看到, GC 線程的工作區間和用戶線程的工作區間是有重疊的(用工作區間這個概念去理解其中的一些細節不一定正確,但是可以這樣抽象的認為,方便理解)。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

而重疊的部分,就是可能產生“對象消失”的部分。對G1來說,就是原始快照(STAB)加寫前屏障(Pre-Wirte Barrier)工作的部分。

所以這就是書裡為什麼說:當 GC 線程掃描完對象圖後,還需要重新處理 STAB 記錄下的在併發時有引用變動的對象。

最終標記(Remark)

書中是這樣的寫的:

最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的 SATB 記錄。

最終標記階段,由於是 STW 的,所以該階段對應的圖是併發標記階段完成後的圖,如下:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

處理併發階段結束後仍遺留下來的最後那少量的 SATB 記錄是什麼意思呢?

你想,併發標記階段, GC 線程完成對象圖的掃描之後,還會去處理 SATB 記錄下的在併發時有引用變動的對象。

在處理 SATB 記錄的數據的時候,由於用戶線程可能還是在繼續修改對象圖,繼續在產生 SATB 數據,所以還是會有一小部分的 SATB 數據,所以才需要一個短暫的暫停。

清理階段(Cleanup)

書裡寫的是篩選回收階段。其實就包含了清理階段和回收階段。這裡我們只討論清理階段,不討論回收。

在這個階段, NextBitmap 和 PrevBitmap 會交換位置:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

所以,我們的圖就變成了下面的樣子:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

可以看到,NextBitmap 和 PrevBitmap 交換了位置,NextTAMS 和 PrevTAMS 交換了位置。

而 Region 中, Bitmap 白色部分對應的已使用內存變成了淺灰色。它僅僅是標記了出來,並沒有進行清掃操作。

需要注意的是:清理階段不拷貝任何對象

引用R大的回答來描述這個階段:

清點和重置標記狀態。這個階段有點像 mark-sweep 中的 sweep 階段,不過不是在堆上 sweep 實際對象,而是在 marking bitmap 裡統計每個 Region 被標記為活的對象有多少。這個階段如果發現完全沒有活對象的 Region 就會將其整體回收到可分配 Region 列表中。

好了,到這裡我們就能把前面的那張圖給填上了:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

然後再看一下論文中的這張圖片,你就會發現,我上面的過程都是基於這張圖片去分析的,圖中展示了兩個循環, A-B-C , D-E-F 。其中 E、F 過程就是 B、C 過程的重複:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

我讓上面的圖片動起來,請細細品。請注意各個階段 PrevTAMS 、 NextTAMS 指針的交換、 PrevBitmap 和 NextBitmap 位置的交換:

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

1.https://max.book118.com/html/2018/0815/7043143036001143.shtm

2.https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

3.https://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html

4.https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/

5.https://hllvm-group.iteye.com/group/topic/44381

6.《深入理解Java虛擬機(第三版)》

最後說一句(求關注)

更新:本文有描述錯誤的地方,詳情見文章

本文是對《面試官:你說你熟悉jvm?那你講一下併發的可達性分析》這篇文章的補充說明。如果你沒看過,我建議你去看看。

我覺得有些知識點僅僅靠文章和圖片是很難說清楚的,所以我費勁的做了動圖。

為了做這篇文章和上篇文章中的幾張動圖,加起來我截了 80 多張圖。你知道我為了把每張圖截的一個像素都不差,我有多努力嗎?

截的我眼球佈滿了血絲,眼睛都快瞎了,你不關注一波?

我四級半的英語水平,為了文章的正確性,強行啃英文論文,你不感動嗎?

不求讚賞,只求文末點“在看”。別白嫖我啊,大哥。寫文章很辛苦的,需要來點正反饋。

面試官:G1回收器怎麼知道你是什麼時候的垃圾?

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

我是why技術,一個不是大佬,但是喜歡分享,又暖又有料的四川好男人。

以上。


分享到:


相關文章: