圖解 Go GC 內存標記法

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French

ℹ️ 本文基於 Go 1.13。關於內存管理的概念的討論在我的文章 Go 中的內存管理和分配 [1] 中有詳細的解釋。

Go GC 的作用是回收不再使用的內存。實現的算法是併發的三色標記和清除回收法。本中文,我們研究三色標記法,以及各個顏色的不同用處。

你可以在 Ken Fox 的 解讀垃圾回收算法 [2] 中瞭解更多關於不同垃圾回收機制的信息。

本文是 Go 語言中文網組織的 GCTT 翻譯,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

標記階段

這個階段瀏覽內存來了解哪些塊兒是在被我們的代碼使用和哪些塊兒應該被回收。

然而,因爲 GC 和我們的 Go 程序並行,GC 掃描期間內存中某些對象的狀態可能被改變,所以需要一個檢測這種可能的變化的方法。爲了解決這個潛在的問題,實現了 寫屏障 [3] 算法,GC 可以追蹤到任何的指針修改。使寫屏障生效的唯一條件是短暫終止程序,又名 “Stop the World”。

在進程啓動時,Go 也在每個 processor 起了一個標記 worker 來輔助標記內存。

然後,當 root 被加入到處理隊列中後,標記階段就開始遍歷和用顏色標記內存。

爲了瞭解在標記階段的每一步,我們來看一個簡單的程序示例:

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{}
}

struct2 不包含指針,因此它被儲存在一個專門存放不被其他對象引用的對象的 span 中。

不包含指針的結構體儲存在專有的 span 中

這減少了 GC 的工作,因爲標記內存時不需要掃描這個 span。

分配工作結束後,我們的程序強迫 GC 重複前面的步驟。下面是流程圖:

掃描內存

GC 從棧開始,遞歸地順着指針找指針指向的對象,遍歷內存。掃描到被標記爲 no scan 的 span 時,停止掃描。然而,這個工作是在多個協程中完成的,每個指針被加入到一個 work pool 中的隊列。然後,後臺運行的標記 worker 從這個 work pool 中拿到前面出列的 work,掃描這個對象然後把在這個對象裏找到的指針加入到隊列。

garbage collector work pool

顏色標記

worker 需要一種記錄哪些內存需要掃描的方法。GC 使用一種 三色標記算法 [4],工作流程如下:

這個初始步驟完成後,GC 會:

然後,GC 重複以上兩步,直到沒有對象可被標記。在這一時刻,對象非黑即白,沒有灰色。白色的對象表示沒有其他對象引用,可以被回收。

下面是前面例子的圖示:

初始狀態下,所有的對象被認爲是白色的。然後,遍歷到的且被其他對象引用的對象,被標記爲灰色。如果一個對象在被標記爲 no scan 的 span 中,因爲它不需要被掃描,所以可以標記爲黑色。

現在灰色的對象被加入到掃描隊列並被標記爲黑色:

對加入到掃描隊列的所有對象重複做相同的操作,直到沒有對象需要被處理:

處理結束時,黑色對象表示內存中在使用的對象,白色對象是要被回收的對象。我們可以看到,由於 struct2 的實例是在一個匿名函數中創建的且不再存在於棧上,因此它是白色的且可以被回收。

歸功於每一個 span 中的名爲 gcmarkBits 的 bitmap 屬性,三色被原生地實現了,bitmap 對 scan 中相應的 bit 設爲 1 來追蹤 scan。

我們可以看到,黑色和灰色表示的意義相同。處理的不同之處在於,標記爲灰色時是把對象加入到掃描隊列,而標記爲黑色時,不再掃描。

GC 最終 STW,清除每一次寫屏障對 work pool 做的改變,繼續後續的標記。

你可以在我的文章 Go GC 怎樣監控你的應用 [5] 中找到關於併發處理和 GC 的標記階段更詳細的描述

runtime 分析器

Go 提供的工具使我們可以對每一步進行可視化,觀察 GC 在我們的程序中的影響。開啓 tracing 運行我們的代碼,可以看到前面所有步驟的一個概覽。下面是追蹤結果:

traces of the garbage collector

標記 worker 的生命週期也可以在追蹤結果中以協程等級可視化。下面是在啓動之前先在後臺等待標記內存的 Goroutine #33 的例子。

marking worker


via: https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-mark-the-memory-72cfc12c6976

作者:Vincent Blanchon[6] 譯者:lxbwolf[7] 校對:polaris1119[8]

本文由 GCTT[9] 原創編譯,Go 中文網 [10] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

Go 中的內存管理和分配: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44

[2]

解讀垃圾回收算法: https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/

[3]

寫屏障: https://en.wikipedia.org/wiki/Write_barrier

[4]

三色標記算法: https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking

[5]

Go GC 怎樣監控你的應用: https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-watch-your-application-dbef99be2c35

[6]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[7]

lxbwolf: https://github.com/lxbwolf

[8]

polaris1119: https://github.com/polaris1119

[9]

GCTT: https://github.com/studygolang/GCTT

[10]

Go 中文網: https://studygolang.com/

福利

我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/t3SffaVmHUb_vqB9pn5yqA