1. 程式人生 > 程式設計 >[譯]Go:垃圾回收器是怎樣標記記憶體的?

[譯]Go:垃圾回收器是怎樣標記記憶體的?

原文:medium.com/a-journey-w…

本文基於Go 1.13

Go的垃圾回收器負責將那些不會再使用的被佔用的記憶體進行回收。實現的演演算法是併發的三色標記法以及掃描收集器。我們會看一下標記階段的細節以及不同顏色的使用。

你可以在這篇文章中閱讀到不同型別的垃圾回收機制。

標記階段

這個階段主要是掃描記憶體來確認哪一些記憶體塊是仍然被使用,在哪一些記憶體塊是可以被回收的。

然而,由於垃圾回收跟我們的Go程式是併發執行的,所以需要有個方法在掃描進行的同時監測記憶體的變化。為瞭解決這個問題,這裡會用到寫屏障演演算法並允許Go去跟蹤任何一個指標的變化。實現寫屏障唯一途徑是將程式暫時停止一小段時間,我們稱為“全世界靜止” (Stop the World)。

在程式執行的開始階段,每一個processor都有一個負責標記記憶體的worker。

然後,一旦根節點被入隊等待執行,標記階段就會開始對記憶體進行遍歷和著色。

下面讓我們看個小的例子,這個程式允許我們能夠遵循標記階段所完成的步驟

type struct1 struct {
	a,b int64
	c,d float64
	e *struct2
}

type struct2 struct {
	f,g int64
	h,i float64
}

func main() {
	s1 := allocStruct1()
	s2 := allocStruct2()

	func () {
		_ = allocStruct2()
	}()

	runtime.GC()

	fmt.Printf("s1 = %X,s2 = %X\n"
,&s1,&s2) } //go:noinline func allocStruct1() *struct1 { return &struct1{ e: allocStruct2(),} } //go:noinline func allocStruct2() *struct2 { return &struct2{} } 複製程式碼

由於結構體subStruct內部不包含任何指標,所以會儲存在一個沒有指向另一個物件的記憶體塊中:

這會讓垃圾清理器更加容易因為當他進行記憶體掃描的時候不需要去掃描這些記憶體塊。

一旦分配完成,我們的程式會強制讓垃圾回收執行一個週期,下面是工作流:

記憶體掃描

垃圾回收器從棧開始,會追隨指標去遞迴遍歷記憶體。那些被標記為no scan的記憶體塊會讓掃描停止繼續掃描。然而,這個過程不是在一個goroutine中完成的。每個指標會在一個垃圾回收器工作池中入隊,被goroutine鎖消耗出隊,出隊後找到新的指向再將其重新在垃圾回收器工作池中入隊,直至遇到no scan為止。

垃圾回收器工作池

著色

worker現在需要有一個途徑去跟蹤哪些記憶體已經被掃描過而哪些還沒有被掃描、垃圾回收器使用三色標記法如下:

  • 最開始階段所有物件標記成白色
  • 根物件(堆、棧、全域性變數)會被標記成灰色

這兩步都完成以後,垃圾回收器會:

  • 拿一個灰色的物件,標記成黑色
  • 跟蹤這個物件的指標並將其所指向的所有物件都標記成灰色

然後,重複這兩個步驟直到沒有可以被著色的物件存在為止。從這個角度出發,物件要麼是黑色,要麼是白色。白色物件代表並沒有任何被其他物件的引用,即可以被清除。

這裡有個上面步驟的展示

一開始所有物件都是白色,然後從根節點開始遞迴,所有沿途物件標記成灰色。如果一個物件被標記成no scan,那可以將它塗成黑色,因為他不需要被繼續往後掃描:

現在灰色物件可以入隊等待掃描並且轉成黑色:

物件以同樣的處理方法入隊直到沒有任何物件需要被處理:

在處理最後一個物件時,黑色的物件就是那個正在使用的記憶體,而白色的物件就是可以被回收的記憶體。如我們所見,由於struct2的例項是在一個匿名函式中建立的,並且不能從根節點沿著指標追蹤得到,所以他會一直是白色,最後被回收。

著色操作能得以實現歸功於每個記憶體塊中叫做gcmarkBits的位,這個位用來將跟蹤掃描過的地方設成1:

如我們所見,黑色與灰色是同樣的工作方式。在處理上不同的地方是,灰色是可以被入隊掃描的,而黑色是指向鏈的尾部。

以上步驟完成以後,垃圾回收器會啟動Stop the world,啟用寫屏障,將期間的記憶體改變情況全部入隊垃圾回收器工作池,然後將這些入隊的記憶體重複以上的步驟進行標記。

執行時分析器 Runtime profiler

這是一個由Go提供的工具,允許我們視覺化每一步垃圾回收的過程,並看到垃圾回收是對我們程式的影響有多大。使用這個跟蹤工具執行我們專案程式碼能夠還能提供強大的視覺化結果,下面是跟蹤圖

標記執行緒的生命週期同樣可以以goroutinue級別進行視覺化。這是goroutine#33的示例,它在開始標記記憶體之前先在後臺等待。