垃圾回收之三色標記法(Tri-color Marking)

邴越發表於2023-04-04

關於垃圾回收演算法,基本就是那麼幾種:標記-清除、標記-複製、標記-整理。在此基礎上可以增加分代(新生代/老年代),每代採取不同的回收演算法,以提高整體的分配和回收效率。

無論使用哪種演算法,標記總是必要的一步。你不先找到垃圾,怎麼進行回收?今天一起看下三色標記法。

先看一下知識點導圖:

 

 

 

 

 

一、如何標記

在 GC 領域裡,判斷物件存活的主流思路是兩個,「引用計數」和「可達性分析」。

1、引用計數

顧名思義,引用計數的思路就是給每個物件進行計數,每被其它物件引用一次,計數就 +1,引用失效後,計數就 -1。當計數器的數值為 0,就意味著它沒有被使用,可以回收。

2、可達性分析

可達性分析的思路就是透過引用鏈路判斷物件是否可被觸達,如果能觸達說明該物件當前正在被使用,不可回收;反之,沒有觸達到的物件則認為是無使用的,可以回收。

這個引用鏈路的結構類似於有向有環圖,但是根節點不止一個,是一個集合,稱之為 GCRoots。

目前主流的 GC 機制大多用的是「可達性分析」這條路線。

 

為什麼引用計數不好用呢?因為它有一個特別嚴重的問題:無法處理迴圈引用。

 

 

 

像上圖這樣的情況,引用計數永遠不為 0,這些物件就永遠不會被回收。

 

 

二、常規標記-清除

 

常規的標記清除嚴格按照追蹤式演算法的思路來實現的。這個演算法會設定一個標誌位來記錄物件是否被使用。最開始所有的標記位都是 0,如果發現物件是可達的就會置為 1,一步步下去就會呈現一個類似樹狀的結果。

 

等標記的步驟完成後,會將未被標記的物件統一清理,再次把所有的標記位設定成 0 方便下次清理。

 

標記清除法主要包含兩個步驟:

  • 標記
  • 清除

示例如下:

1、開啟STW,停止程式的執行,圖中是本次GC涉及到的root節點和相關物件。

 

 

 

 

 

2、從根節點出發,標記所有可達物件。

 

 

3、停止STW,然後回收所有未被標記的物件

 

 

 

 

這樣執行整個GC期間需要STW,將整個程式暫停。因為如果不進行STW的話,會出現已經被標記的物件A,引用了新的未被標記的物件B,但由於物件A已經標記過了,不會再重新掃描A對B的可達性,從而將B物件當做垃圾回收掉的問題。

 

三、三色標記

垃圾收集器依據可達性分析演算法判斷物件是否存活時,將遍歷GC Roots過程中遇到的物件,按照“是否訪問過”這個條件,把物件標記成白色(white)、灰色(gray)、黑色(black)三種顏色,這個標記過程稱為三色標記法。

相比傳統的標記清掃演算法,三色標記最大的好處是可以非同步執行,從而可以以中斷時間極少的代價或者完全沒有中斷來進行整個 GC。

 

1、基本演算法

 

三色標記法將物件用三種顏色表示,分別是白色、灰色和黑色。

最開始所有物件都是白色的,然後把其中全域性變數和函式棧裡的物件置為灰色。

第二步把灰色的物件全部置為黑色,然後把原先灰色物件指向的變數都置為灰色,以此類推。

等發現沒有物件可以被置為灰色時,所有的白色變數就一定是需要被清理的垃圾了。

 

 

  • 初始標記階段,指的是標記 GCRoots 直接引用的節點,將它們標記為灰色,這個階段需要 「Stop the World」。
  • 併發標記階段,指的是從灰色節點開始,去掃描整個引用鏈,然後將它們標記為黑色,這個階段不需要「Stop the World」。
  • 重新標記階段,指的是去校正併發標記階段的錯誤,這個階段需要「Stop the World」。
  • 併發清除,指的是將已經確定為垃圾的物件清除掉,這個階段不需要「Stop the World」。

 

三色標記法是一個 false negative(假陰性)的演算法:

  • 三色標記法因為多了一個白色的狀態來存放不確定的物件,所以可以非同步地執行。
  • 當然非同步執行的代價是可能會造成一些遺漏,因為那些早先被標記為黑色的物件可能目前已經是不可達的了。

 

2、現代垃圾回收器實現

現代追蹤式(可達性分析)的垃圾回收器幾乎都借鑑了三色標記的演算法思想,儘管實現的方式不盡相同:比如白色/黑色集合一般都不會出現(但是有其他體現顏色的地方)、灰色集合可以透過棧/佇列/快取日誌等方式進行實現、遍歷方式可以是廣度/深度遍歷等等。

對於讀寫屏障,以Java HotSpot VM 為例,其併發標記時對漏標的處理方案如下:

  • CMS:寫屏障 + 增量更新
  • G1:寫屏障 + SATB
  • ZGC:讀屏障

 

四、多標及漏標問題

三色標記演算法缺陷:在併發標記階段的時候,因為使用者執行緒與GC執行緒同時執行,有可能會產生多標或者漏標;

  • 多標--多標記(浮動垃圾)
  • 漏標--漏標記

 

1、多標問題

併發標記:使用者與GC執行緒同時執行,假設現在掃描到C物件,B物件變為黑色,使用者執行緒執行C的屬性E=null,GC執行緒掃描C物件引用鏈,認為E物件是為可達物件,但是C物件根本沒有引入到E物件,E物件應該是為垃圾物件,這種問題,可以在重新標記階段(修正)修復。

 

併發清除階段:使用者與GC執行緒同時執行,會產生新的物件但是沒有及時被GC清理。

多標只能在下一次GC清理垃圾的修復。

2、漏標問題

 

1.使用者執行緒先執行C的E屬性=null;GC執行緒的GcRoot就掃描不到E。Gc就認為E物件就是為垃圾物件,不可達物件。
2.使用者線有執行B.E屬性=E;E物件就是應該是為可達物件。
3.因為GCRoot是從C開始,不會從黑色的B開始,就會導致漏標的情況發生。

 

漏標的問題滿足兩個條件:

  1. 有至少一個黑色物件在自己被標記之後指向了這個白色物件
  2. 所有的灰色物件在自己引用掃描完成之前刪除了對白色物件的引用
 
 只有當上面兩個條件都滿足,三色標記演算法才會發生漏標的問題。換言之,如果我們破壞任何一個條件,這個白色物件就不會被漏標。
 

CMS如何解決漏標問題---寫屏障+增量更新方式

滿足一個條件(灰色物件與白色物件斷開連線),在併發標記階段當我們黑色物件(B)引用關聯白色物件(E),記錄下B黑色物件。
在重新標記階段(所有使用者執行緒暫停),有將B物件變為灰色物件將整個引用鏈全部掃描。
缺點:遍歷B整個鏈的效率非常低,有可能會導致使用者執行緒等待的時間非常長。

 

G1如何解決漏標問題---原始快照方式

在C斷開E的時候,會記錄原始快照,在重新標記階段的時候以白色物件變為灰色為起始點掃描整個鏈,本次GC是不會被清理。
好處:如果假設B(黑色物件)引入該白色物件的時候,無需做任何遍歷效率是非常高。
缺點:如果假設B(黑色物件) 沒有引入該白色物件的時候,該白色物件在本次GC繼續存活,只能放在下一次GC在做併發標記的時候清理。
tips:以浮動垃圾(佔記憶體空間)換讓我們使用者執行緒能夠暫停的時間更加短。

 

總結:

 

對於讀寫屏障,以Java HotSpot VM為例,其併發標記時對漏標的處理方案如下:

  • CMS:採用的是寫屏障 + 增量更新
  • G1: 採用的是寫屏障 + 原汁快照(SATB)
  • ZGC:採用的是讀屏障

CMS收集器解決漏標問題:增量方式 如果現在B(黑色)物件引入白色物件,寫屏障。

好處:避免浮動垃圾,缺點掃描整個引用鏈效率比較低。

 

G1收集器解決漏標問題:原始快照方式。

好處:效率非常高,無需掃描整個引用鏈,缺點:可能會產生浮動垃圾。

 

參考資料:

https://en.wikipedia.org/wiki/Tracing_garbage_collection

https://www.cnblogs.com/jmcui/p/14165601.html

 

相關文章