『每周譯Go』golang 垃圾回收器如何標(biāo)記內(nèi)存?

本文基于 Go 1.13。這里討論的關(guān)于內(nèi)存管理的概念在我的文章Go:內(nèi)存管理和分配 中有解釋
Go 垃圾回收器負(fù)責(zé)回收不再使用的內(nèi)存。實現(xiàn)的算法是一個并行的三色標(biāo)記掃描采集器。在本文中,我們將詳細(xì)了解標(biāo)記階段,以及不同顏色的用法。
您可以在 kenfox 的可視化垃圾回收算法 中找到關(guān)于不同類型垃圾回收器的更多信息。
標(biāo)記階段
此階段執(zhí)行內(nèi)存掃描,以了解代碼仍在使用哪些塊,以及應(yīng)該回收哪些塊。
但是,由于垃圾回收器可以與我們的 Go 程序同時運行,因此它需要一種在掃描時檢測內(nèi)存中潛在變化的方法。為了解決這個潛在的問題,實現(xiàn)了一個寫屏障算法,允許 Go 跟蹤任何指針的變化。啟用寫屏障的唯一條件是短時間停止程序,也稱為 “STW”:

在進(jìn)程開始時,Go 還會為每個處理器啟動一個標(biāo)記輔助進(jìn)程,以幫助標(biāo)記內(nèi)存。
然后,一旦根節(jié)點被排隊等待處理,標(biāo)記階段就可以開始遍歷內(nèi)存并為其著色。
現(xiàn)在讓我們以一個簡單的程序為例,該程序允許我們遵循標(biāo)記階段所做的步驟
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{}
}
由于 struct subStruct 不包含任何指針,因此它存儲在一個專用于對象的范圍中,而不引用其他對象:

這使得垃圾回收器的工作更容易,因為它在標(biāo)記內(nèi)存時不必掃描這個范圍。
一旦分配完成,我們的程序就會強(qiáng)制垃圾回收器運行一個周期。以下是工作流程:

垃圾回收器從堆棧開始標(biāo)記,然后跟著指針遞歸遍歷內(nèi)存。直到對象都被標(biāo)記時停止掃描。然而,這個過程不是在同一個 goroutine 中完成的 完成的;每個指針都在工作池中排隊。然后 ,后臺的標(biāo)記線程發(fā)現(xiàn)之前的出列隊列是來自該工作池,掃描對象,然后將在其中找到的指針加入隊列:
著色!
后臺線程現(xiàn)在需要一種方法來跟蹤哪些內(nèi)存有沒有被掃描。垃圾回收器使用三色算法,其工作原理如下:
所有對象一開始都被認(rèn)為是白色的
根對象(堆棧、堆、全局變量)將以灰色顯示
完成此主要步驟后,垃圾回收器將:
選擇一個灰色的對象,把它涂成黑色
遵循此對象的所有指針并將所有引用的對象涂成灰色
然后,它將重復(fù)這兩個步驟,直到?jīng)]有更多的對象要著色。從這一點來看,對象不是黑色就是白色。白色集合表示未被任何其他對象引用且準(zhǔn)備好回收的對象。
下面是使用上一個示例對其進(jìn)行的表示:

作為第一種狀態(tài),所有對象都被視為白色。然后,對象被遍歷,可到達(dá)的對象將變?yōu)榛疑H绻麑ο笪挥跇?biāo)記為 “無掃描” 的范圍內(nèi),則可以將其繪制為黑色,因為不需要對其進(jìn)行掃描:

灰色對象現(xiàn)在入隊等待掃描并變黑:

在沒有更多的對象要處理之前,入隊的對象也會發(fā)生同樣的情況:

在進(jìn)程結(jié)束時,黑色對象是內(nèi)存中正在使用的對象,而白色對象是要回收的對象。如我們所見,由于 struct2 的實例是在匿名函數(shù)中創(chuàng)建的,并且無法從堆棧訪問,因此它保持為白色,可以清除。
由于每個跨度中有一個名為 gcmarkBits 的位圖屬性,顏色在內(nèi)部實現(xiàn),該屬性跟蹤掃描,并將相應(yīng)的位設(shè)置為 1:

正如我們所見,黑色和灰色的工作原理是一樣的。這一過程的不同之處在于,當(dāng)黑色對象結(jié)束掃描鏈時,灰色對象排隊等待掃描。
垃圾回收器最終會 stops the world,將每個寫屏障上所做的更改刷新到工作池,并執(zhí)行剩余的標(biāo)記。
您可以在我的文章Go:垃圾回收器如何監(jiān)視您的應(yīng)用程序 中找到有關(guān)并發(fā)進(jìn)程和垃圾回收器中標(biāo)記階段的更多詳細(xì)信息
運行時分析器
Go 提供的工具允許我們可視化所有這些步驟,并在程序中查看垃圾回收器的影響。在啟用跟蹤的情況下運行我們的代碼提供了前面步驟的一個大圖。以下是 traces:

標(biāo)記線程的生命周期也可以在 goroutine 級別的 tracer 中可視化。下面是 goroutine#33 的示例,它在開始標(biāo)記內(nèi)存之前先在后臺等待。

