jvm學習 Shenandoah垃圾收集器

系統學習請點擊jvm學習目錄
建議學習Shenandoah之前先學習G1垃圾收集器

前言

Shenandoah垃圾收集器是一個很有意思的垃圾收集器,它是第一款非Oracle公司開發的HotSpot垃圾收集器,以至於Oracle JDK將其排斥在外,所以它暫時只能出現在Open JDK中。它是一款立志於在任何堆內存下都要將垃圾收集的停頓時間限制的很低(10ms,沒有實現~~不過也很厲害了,這也意味着對於吞吐量自然就···),也就是追求低延遲啦。

Shenandoah垃圾收集器可以說是G1垃圾收集器的一個修改版了(不能說是改進,只能說是修改,因爲Shenandoah更激進,一味追求低延遲),所以,它與G1有着高度相似,當然爲了追求低延遲,它也有一些修改的地方。

下面我們就將講講Shenandoah與G1區別與聯繫在哪。如果對G1不熟悉的小夥伴可以點擊G1 垃圾收集器快速入門學習

Shenandoah與G1的區別與聯繫

Shenandoah是在G1的基礎上進行了修改,從而向低延遲的目標進發,回憶之前我們博客中講G1的時候,在其Mixed GC階段,主要分爲初始標記、併發標記、重新標記、清除垃圾和最終回收階段(evacuation),其中5個有3個都是STW的,除了併發標記其他都是STW,那麼爲了追求低延時,此刻Shenandoah選擇將其中停頓時間較大的最終回收階段給變成非STW的,也就是併發的,用戶線程與垃圾回收線程同時運行。
這一塊內容便是Shenandoah與G1的核心不同之處。我們下面一小節會詳細的講。


Shenandoah繼承了G1的堆內存劃分,也就是將堆內存劃分成了一個個大小相等的Region,也有着存放大對象的Humongous區域,但是呢,Shenandoah不遵循分代理論,也就是說在Shenandoah立即收集器的規則裏,沒有老年代新生代一說了。不過在進行垃圾回收時,依然是選取回收效率高的Region回收。(沒了分代理論了,自然也就沒有G1中的young GC了,只剩下Mixed GC)。


Shenandoah垃圾回收器放棄了G1中的記憶集(卡表實現),改從用連接矩陣這種數據結構來解決跨Region引用的問題。
所謂連接矩陣,這裏其實就是圖論中講到的鄰接矩陣啦。
舉個例子來說一下吧:
假設現在有A、B、C、D四個Region,其中B中的對象引用了D,D中對象引用了A,那麼用連接矩陣就是這樣表示:
在這裏插入圖片描述
顯然,通過這個連接矩陣,我們可以很方便的獲得跨Region的引用情況,比起每個Region都維護一個卡表可以說方便很多,而且也節省了資源。
奇思妙想嘻嘻:這裏的連接矩陣都是0101的,感覺似乎可以用矩陣分解來降低維度,從而進一步節省存儲空間,不過似乎沒有必要,計算還更麻煩了

Shenandoah工作過程

Shenandoah收集器的工作過程可以說大致上是和G1垃圾收集器中的差不多的,主要區別就是在於最終回收階段啦,這裏在Shenandoah中是併發進行的,所以我們稱之爲併發回收階段(Concurrent Evacuation)。

Shenandoah垃圾回收期的工作過程可以大致劃分爲五個階段:

  1. 初始標記(Initial Mark)
  2. 併發標記(Concurrent Mark)
  3. 重新標記(Final Mark)
  4. 併發清理(Concurrent Cleanup)
  5. 併發回收(Concurrent Evacuation)

下面對每個階段分別進行介紹。

初始標記階段:該階段是標記GC ROOTS直接可達的對象。因爲和G1不同,沒有了young GC,沒法借道,所以這裏是需要STW的,不過時間非常短暫。

併發標記階段:和用戶線程一起併發工作,在可達性數上進行掃描,確認對象們的存活狀態。該階段是不需要STW的。

重新標記階段:與G1一樣,將在併發標記中被用戶修改引用關係的對象重新掃描,避免出現併發可達性分析的安全問題。這裏採用的是原始快照。同時,統計出回收價值最高的Region,將這些Region加入回收集。這個階段當然是會STW的。

併發清理階段:這個階段和G1有點不同,因爲在G1中,該階段STW,而在Shenandoah中,卻沒有,該階段作用一樣,也是來清理回收集中那些無存活對象的Region。該階段不需要STW。

併發回收階段:該階段是Shenandoah與G1的核心差異所在。將回收集裏的存活對象複製到其他未使用的Region中,然後將原Region回收。看到這,你可能會說,這和G1有什麼區別呢?G1也是做這些呀。
不一樣,G1是STW之後,來複制對象,當然,這個階段時間不短,這樣的操作會十分的簡單。
而在Shenandoah中,它不需要STW,也就是該階段在Shenandoah中是併發的,哦喲,這可了不得,因爲要知道,這會出併發安全問題的。所以針對此,Shenandoah進行了專門的安排。
咱們在下一小節來咱們講一講併發回收階段的細節。

併發回收階段的細節

在併發過程中實現存活對象的複製,其實是一個很困難的事情。這裏主要存在兩個問題:

  1. 複製完成之後,用戶線程訪問對象,是訪問新對象還是訪問舊的對象呢?如果是訪問新對象怎麼操作呢?
  2. 在併發過程中,如果出現併發安全問題,如何解決?

首先來解決第一個問題
在存活對象完成複製之後,用戶線程訪問對象當然是要訪問新的對象啦,因爲此時舊對象的Region已經被視爲可回收空間了嘛,當然不可能再用。
那麼此時,對象的引用還是指向的是舊對象的地址,如何訪問到新的對象呢?Shenandoah提出了一個辦法:“Brooks Pointer”。
該辦法就是在每個對象的最前面加上一個新的引用字段。這個引用字段指向對象。對於未移動的對象來說,它的引用指向自己。我們用一個示意圖來說明一下。
如下圖所示,引用指向自己。這也就意味着,在引入Brooks Pointer這個概念之後,我們訪問一個對象的流程變成了:通過變量中存儲的地址找到Brooks Pointer,再通過Brooks Pointer找到對象。
在這裏插入圖片描述
而經過了移動的對象,舊對象的Brooks Pointer則指向新對象。如下圖所示。
此時訪問對象的流程是:通過變量中存儲的地址找到舊對象的Brooks Pointer,再通過舊對象的Brooks Pointer找到新對象的Brooks Pointer,再通過新對象的Brooks Pointer找到新對象。
在這裏插入圖片描述
如此,便解決了訪問新對象的問題。
不過,這種方法也是有弊端的,很顯然的就是將原本簡單的對象訪問流程變的更加繁瑣,本來一步就能訪問到對象,現在得兩步,你說麻煩不麻煩。不過由於複製存活對象這事幹的挺多,所以其實也還好,總體還是挺好的。


下面來解決第二個問題。
併發安全問題。在上面的問題的基礎上,我們來設想這樣一種情況:

  1. 垃圾收集線程複製了新的對象
  2. 用戶線程更新了對象
  3. 垃圾收集線程將舊對象的Brooks Pointer指向新對象

如此一來,用戶的更新操作落在了舊對象上,而新對象並未被操作,從而出現了安全問題。所以這個問題必須得解決。這個問題也可以說是一個同步問題,也是比較簡單的,Shenandoah同時設置了讀、寫屏障來解決該問題。保證1,3是必須相繼完成,不能被分割。


說完了上面兩個問題,其實併發回收階段的核心也講的差不多了,接下來就簡單的把併發回收階段的具體流程簡單的過一遍。
併發回收階段流程

  1. 併發複製:利用讀寫屏障和Brooks Pointer,將存活對象複製別的Region中去。
  2. 初始引用更新:設定一個線程集合點,確保併發回收階段所有的收集線程都已經完成它們的對象移動任務。會STW很短一段時間,該階段爲下一階段做準備。
  3. 併發引用更新:開始進行引用更新,將變量中的舊對象內存地址改成新對象的內存地址。沿着內存物理地址順序進行。
  4. 最終引用更新:修正GC Roots中的引用,該階段短暫的STW。
  5. 併發清理:將回收集中的Region回收。

總結

Shenandoah可以將總停頓時間限制的很低,相較G1、CMS還有其他的一些垃圾收集器,其可以說是低延遲了,很好的符合了當今硬件較強的時代,但是缺點也是顯然,其吞吐量不如其他垃圾收集器。

總體來說Shenandoah和G1是非常相似的,在內存佈局上,在整體的流程上,包括優先收集效益高的Region這一策略,都是非常相似的,但是也是有着顯著不同的,比如回收階段一個是並行,一個是併發,比如記憶集改成了連接矩陣。
總之,二者的目標是不一樣的,可以說是發展方向不同的兩兄弟(就好像佐助和鳴人一樣)。

參考資料

  • 《深入理解jvm》周志明
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章