golang 垃圾回收器如何標記記憶體?

cuua發表於2021-06-29

本文基於 Go 1.13。這裡討論的關於記憶體管理的概念在我的文章Go:記憶體管理和分配 中有解釋

Go 垃圾回收器負責回收不再使用的記憶體。實現的演算法是一個並行的三色標記掃描採集器。在本文中,我們將詳細瞭解標記階段,以及不同顏色的用法。

您可以在 kenfox 的視覺化垃圾回收演算法 中找到關於不同型別垃圾回收器的更多資訊。

標記階段

此階段執行記憶體掃描,以瞭解程式碼仍在使用哪些塊,以及應該回收哪些塊。

但是,由於垃圾回收器可以與我們的 Go 程式同時執行,因此它需要一種在掃描時檢測記憶體中潛在變化的方法。為了解決這個潛在的問題,實現了一個寫屏障演算法,允許 Go 跟蹤任何指標的變化。啟用寫屏障的唯一條件是短時間停止程式,也稱為 “STW”:

在程式開始時,Go 還會為每個處理器啟動一個標記輔助程式,以幫助標記記憶體。

然後,一旦根節點被排隊等待處理,標記階段就可以開始遍歷記憶體併為其著色。

現在讓我們以一個簡單的程式為例,該程式允許我們遵循標記階段所做的步驟

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 不包含任何指標,因此它儲存在一個專用於物件的範圍中,而不引用其他物件:

這使得垃圾回收器的工作更容易,因為它在標記記憶體時不必掃描這個範圍。

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

垃圾回收器從堆疊開始標記,然後跟著指標遞迴遍歷記憶體。直到物件都被標記時停止掃描。然而,這個過程不是在同一個 goroutine 中完成的;每個指標都在工作池中排隊。然後 ,後臺的標記執行緒發現之前的出列佇列是來自該工作池,掃描物件,然後將在其中找到的指標加入佇列:

著色!

後臺執行緒現在需要一種方法來跟蹤哪些記憶體有沒有被掃描。垃圾回收器使用三色演算法,其工作原理如下:

  • 所有物件一開始都被認為是白色的

  • 根物件(堆疊、堆、全域性變數)將以灰色顯示

完成此主要步驟後,垃圾回收器將:

  • 選擇一個灰色的物件,把它塗成黑色

  • 遵循此物件的所有指標並將所有引用的物件塗成灰色

然後,它將重複這兩個步驟,直到沒有更多的物件要著色。從這一點來看,物件不是黑色就是白色。白色集合表示未被任何其他物件引用且準備好回收的物件。

下面是使用上一個示例對其進行的表示:

作為第一種狀態,所有物件都被視為白色。然後,物件被遍歷,可到達的物件將變為灰色。如果物件位於標記為 “無掃描” 的範圍內,則可以將其繪製為黑色,因為不需要對其進行掃描:

灰色物件現在入隊等待掃描並變黑:

在沒有更多的物件要處理之前,入隊的物件也會發生同樣的情況:

在程式結束時,黑色物件是記憶體中正在使用的物件,而白色物件是要回收的物件。如我們所見,由於 struct2 的例項是在匿名函式中建立的,並且無法從堆疊訪問,因此它保持為白色,可以清除。

由於每個跨度中有一個名為 gcmarkBits 的點陣圖屬性,顏色在內部實現,該屬性跟蹤掃描,並將相應的位設定為 1:

正如我們所見,黑色和灰色的工作原理是一樣的。這一過程的不同之處在於,當黑色物件結束掃描鏈時,灰色物件排隊等待掃描。

垃圾回收器最終會 stops the world,將每個寫屏障上所做的更改重新整理到工作池,並執行剩餘的標記。

您可以在我的文章Go:垃圾回收器如何監視您的應用程式 中找到有關併發程式和垃圾回收器中標記階段的更多詳細資訊

執行時分析器

Go 提供的工具允許我們視覺化所有這些步驟,並在程式中檢視垃圾回收器的影響。在啟用跟蹤的情況下執行我們的程式碼提供了前面步驟的一個圖片。以下是 traces:

標記執行緒的生命週期也可以在 goroutine 級別的 tracer 中視覺化。下面是 goroutine#33 的示例,它在開始標記記憶體之前先在後臺等待。

更多原創文章乾貨分享,請關注公眾號
  • golang 垃圾回收器如何標記記憶體?
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章