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

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

本文基於Go 1.13算法

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

你能夠在這篇文章中閱讀到不一樣類型的垃圾回收機制。函數

標記階段

這個階段主要是掃描內存來確認哪一些內存塊是仍然被使用,在哪一些內存塊是能夠被回收的。工具

然而,因爲垃圾回收跟咱們的Go程序是併發運行的,因此須要有個方法在掃描進行的同時監測內存的變化。爲了解決這個問題,這裏會用到寫屏障算法並容許Go去跟蹤任何一個指針的變化。實現寫屏障惟一途徑是將程序暫時中止一小段時間,咱們稱爲「全世界靜止」 (Stop the World)。atom

在程序運行的開始階段,每個processor都有一個負責標記內存的worker。spa

而後,一旦根節點被入隊等待執行,標記階段就會開始對內存進行遍歷和着色。線程

下面讓咱們看個小的例子,這個程序容許咱們可以遵循標記階段所完成的步驟3d

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的示例,它在開始標記內存以前先在後臺等待。

相關文章
相關標籤/搜索