天天看點

Go的垃圾回收算法(三色标記法)

01  判斷對象存活的思路

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

01 引用計數

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

02 可達性分析

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

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

目前主流的 GC 機制大多用的是「可達性分析」這條路線。Go、Java、.Net等都是如此。為什麼引用計數不好用呢?因為它有一個特别嚴重的問題:無法處理循環引用。

Go的垃圾回收算法(三色标記法)

像上圖這樣的情況,引用計數永遠不為 0,這些對象就永遠不會被回收,這會嚴重影響回收的效果。

但是它也并不是一無是處,它的回收實時性效果更好,可以配合「可達性分析」一起使用,發揮各自的優點,在不同的場景下使用不同的政策。

由于,「可達性分析」思路是主流,是以後續發展出來的很多回收算法都以這個思路為基礎的,三色标記法就是其中之一。我們今天主要來聊聊它。

02  三色标記法

在講具體原理之前先了解一個概念,「Stop The World 」,簡稱「STW」。

垃圾回收器的工作流程大體如下:

    1.标記出哪些對象是存活的,哪些是可回收的。

    2.進行回收(清除/複制/整理)。如果在回收期間有移動過的對象(複制/整理),還需要更新引用。

第一步做标記的過程又可以分成兩個步驟。

    1.标記 GC ROOT 能關聯到的對象。這裡會 STW。

    2.從 GCRoots 的直接關聯對象開始周遊整個對象圖。這裡不會STW。

垃圾回收算法主要做的就是第一步中的第二步,三色标記法也不例外,它将從GC Roots 開始周遊的對象标記為以下三種顔色:

   ■ 白色,初始值。本次回收沒被掃描過的對象預設都是白色的。而确認不可達的對象也是白色,但是會被标記「不可達」。

   ■ 灰色,中間狀态。本對象有被外部引用,但是本對象引用的其它對象尚未全部檢測完。

   ■ 黑色,本對象有被其它對象引用,且已檢測完本對象引用的其它對象。

其實這三種顔色是啥不重要的,重要的是它們所表達的狀态,灰色的中間狀态,标記過程結束後隻會存在白色或者黑色。

整個過程中,這些狀态是如下圖這樣變化的。

Go的垃圾回收算法(三色标記法)

看似很完美的解決方案,其實也存在的一個問題:标記過程中,對象引用發生了變化。

它會導緻兩個問題,「多标」和「漏标」。

多标就是下圖這樣:

Go的垃圾回收算法(三色标記法)

由于步驟2不會STW,是以可能存在掃描過A将它标記為黑色後,又重新引用了一個原本已經被标記為白色的D(C斷開了與D的引用)。此時,D就會被回收掉,導緻程式出現意料之外的bug。

「漏标」就是這樣:

Go的垃圾回收算法(三色标記法)

對象 E/F/G 是“應該”被回收的。然而因為 E 已經變為灰色了,其仍會被當作存活對象繼續周遊下去。最終的結果是:這部分對象仍會被标記為存活,即本輪 GC 不會回收這部分記憶體。

傳統的解決這兩個問題的思路有兩個:

    1.在斷開引用的時候做額外處理。

    2.在「黑色」對象重建立立「白色」對象的引用時做額外處理。(回收開始後建立的對象預設為黑色)。

第一個思路專業叫法是「寫屏障」,第二個是「讀屏障」。其實名字就是噱頭,你可以把它們倆當我們平時程式設計中用到的 AOP 概念來了解,在修改和讀取之前做一些操作。

基于「寫屏障」,可以延伸出兩個方案:

   ■ 增量更新(Incremental Update)。針對新增的引用,将其記錄下來等待重新周遊。這個操作在「修改操作後」進行,JVM 中的 CMS 垃圾回收器就是這個思路。

   ■ 原始快照(Snapshot At The Beginning,SATB)。當某個時刻 的 GC Roots 确定後,當時的對象圖就已經确定了。如果期間發生變化,則可以記錄起來,保證标記依然按照原本的視圖來。這個操作在「修改操作前」進行,JVM中 的 G1 垃圾回收器用的就是這個思路。理論上,配合 「Remembered Set」,SATB 的效率是比增量更新要高的,不過會消耗更多的記憶體。

基于「讀屏障」的方案是:在「黑色」對象重建立立「白色」對象的引用前,将這個白色對象記錄下來,避免被回收掉。這個動作在「讀取操作前」進行,JVM 中的 ZGC 垃圾回收器就是這個思路。

在 Golang(1.8版本之後)裡,用的是一種新的機制,稱之為「混合寫屏障」機制。它的思路總結下來就是4句話:

    1.将對象分為堆上的對象和棧上的對象。

    2.GC 開始将棧上的對象全部掃描并标記為黑色,無需 STW。并且之後不再進行第二次重複掃描

    3.在 GC 期間,任何在棧上建立的新對象,均為黑色。

    4.在 GC 期間,在堆上被删除或者添加的對象都标記為灰色。後續繼續掃描。

你看,其實這些原理也沒那麼複雜,我相信隻要你搞清楚了自己面對的是什麼問題,你也能想到這些方案。

好了,總結一下。

這篇呢,Z 哥和你分享了我對 Golang 中的 GC 機制「三色标記法」的了解。

GC 的底層判斷對象存活思路主要是兩個,引用計數和可達性分析。由于引用計數存在循環引用問題,是以大多數 GC 都是按照後者的思路實作的,Golang 也不例外。

「三色标記法」的原理是,将對象分為了三種狀态:

   ■ 白色,預設值。本次回收沒被掃描過的對象都是白色的。确認不可達的對象也是白色,但是會被标記「不可達」。

   ■ 灰色,中間狀态。本對象有被外部引用,但是本對象引用的其它對象尚未全部檢測完。

   ■ 黑色,本對象有被其它對象引用,且已檢測完本對象引用的其它對象。

最終将白色狀态的對象回收掉。為了解決其中會存在的漏标、多标問題,它通過「混合寫屏障」的機制來解決。思路是,

    1.将對象分為堆上的對象和棧上的對象。

    2.GC 開始将棧上的對象全部掃描并标記為黑色,無需 STW。并且之後不再進行第二次重複掃描

繼續閱讀