golang垃圾回收

slowquery發表於2022-10-05

0.1、索引

waterflow.link/articles/1664943418...

文中提到的垃圾回收演算法是基於go1.16之後的,讓我們直接進入正題吧。

1、什麼時候需要垃圾回收?

https://cdn.learnku.com/uploads/images/202210/05/25530/uk3zhnIXoL.png!large

Go 更喜歡在堆疊上分配記憶體,因此大多數記憶體分配最終都會在棧上。 這意味著 Go 每個 goroutine 都有一個堆疊,並且在可能的情況下,Go 會將變數分配給這個堆疊。 Go 編譯器試圖透過執行逃逸分析來檢視物件是否被外部變數引用。 如果編譯器可以確定一個變數的生命週期,它將被分配到一個堆疊中。 但是,如果變數的生命週期不明確,它將在堆上分配。 通常,如果 Go 程式有一個指向物件的指標,則該物件儲存在堆上。 看看這個示例程式碼:

type myStruct struct {
  value int
}
var testStruct = myStruct{value: 0}
func addTwoNumbers(a int, b int) int {
  return a + b
}
func myFunction() {
  testVar1 := 123
  testVar2 := 456
  testStruct.value = addTwoNumbers(testVar1, testVar2)
}
func someOtherFunction() {
  myFunction()
}

我們假設這是一個正在執行的程式的一部分,因為如果這是整個程式,Go 編譯器會透過將變數分配到堆疊中來最佳化它。 程式執行時:

  1. testStruct 被定義並放置在堆上的一個可用記憶體塊中。
  2. myFunction 在函式執行時被執行並分配一個棧。 testVar1 和 testVar2 都儲存在此堆疊中。
  3. 當 addTwoNumbers 被呼叫時,一個新的棧幀被壓入棧中,並帶有兩個函式引數。
  4. 當 addTwoNumbers 完成執行時,它的結果返回給 myFunction 並且 addTwoNumbers 的堆疊幀從堆疊中彈出,因為它不再需要了。
  5. 指向 testStruct 的指標被定為到包含它的堆上的位置,並且值欄位被更新。
  6. myFunction 退出並且為其建立的堆疊被清理。 testStruct 的值會一直保留在堆上,直到發生垃圾回收。

testStruct 現在在堆上並且沒有分析,Go 執行時不知道是否仍然需要它。 為此,Go 依賴於垃圾回收器。 垃圾回收器有兩個關鍵部分,一個 mutator 和一個回收器。 回收器執行垃圾收集邏輯並找到應該釋放其記憶體的物件。 mutator 執行應用程式程式碼並將新物件分配給堆。 它還會在程式執行時更新堆上的現有物件,其中包括在不再需要某些物件時使其無法訪問。
https://cdn.learnku.com/uploads/images/202210/05/25530/4kCynCGTHZ.png!large

2、垃圾回收器的實現

Go 的垃圾收集器是一個非分代併發三色標記清除垃圾回收器。 讓我們分解一下這些術語。

什麼是分代:

由於“複製”演算法對於存活時間長,大容量的儲存物件需要耗費更多的移動時間,和存在儲存物件的存活時間的差異。需要程式將所擁有的記憶體空間分成若干分割槽,並標記為年輕代空間和年老代空間。程式執行所需的儲存物件會先存放在年輕代分割槽,年輕代分割槽會較為頻密進行較為激進垃圾回收行為,每次回收完成倖存的儲存物件內的壽命計數器加一。當年輕代分割槽儲存物件的壽命計數器達到一定閾值或儲存物件的佔用空間超過一定閾值時,則被移動到年老代空間,年老代空間會較少執行垃圾回收行為。一般情況下,還有永久代的空間,用於涉及程式整個執行生命週期的物件儲存,例如執行程式碼、資料常量等,該空間通常不進行垃圾回收的操作。 透過分代,存活在侷限域,小容量,壽命短的儲存物件會被快速回收;存活在全域性域,大容量,壽命長的儲存物件就較少被回收行為處理干擾。——維基百科

分代垃圾回收器專注於最近分配的物件。 但是,如前所述,編譯器最佳化允許 Go 編譯器將具有已知生命週期的物件分配給堆疊。 這意味著更少的物件將在堆上,因此更少的物件將被垃圾回收。 這意味著在 Go 中不需要分代垃圾回收器。 因此,Go 使用了非分代垃圾回收器。 併發意味著回收器與 mutator 執行緒同時執行。 因此,Go 使用非分代併發垃圾回收器。 標記和清除是垃圾回收器的型別,三色是用於實現它的演算法。

Go 透過幾個步驟實現了這一點:

1、開啟寫屏障

Go 透過一個名為 stop the world 的程式讓所有 goroutine 到達垃圾回收安全點。 這會暫時停止程式執行並開啟寫屏障以維護堆上的資料完整性。 這透過允許 goroutine 和回收器同時執行來實現併發。
https://cdn.learnku.com/uploads/images/202210/05/25530/H5DdfAAh66.png!large

想要在併發或者增量的標記演算法中保證正確性,我們需要達成以下兩種三色不變性(Tri-color invariant)中的一種:

  • 強三色不變性 — 黑色物件不會指向白色物件,只會指向灰色物件或者黑色物件;
  • 弱三色不變性 — 黑色物件指向的白色物件必須包含一條從灰色物件經由多個白色物件的可達路徑;
    https://cdn.learnku.com/uploads/images/202210/05/25530/xHRsxR0rSP.png!large

一旦所有的 goroutine 都開啟了寫屏障,Go 執行時就會starts the world並讓workers執行垃圾回收工作。

2、標記階段

標記是透過使用三色演算法實現的。 當標記開始時,根物件是灰色的,其他物件都是白色的。 根是所有其他堆物件的源物件,並作為執行程式的一部分被例項化。 垃圾回收器透過掃描堆疊、全域性變數和堆指標開始標記以瞭解正在使用的內容。 掃描堆疊時,workers 停止 goroutine 並透過從根向下遍歷將所有找到的物件標記為灰色。 掃描完成恢復 goroutine。

三色標記的工作原理:

  1. 從灰色物件的集合中選擇一個灰色物件並將其標記成黑色;
  2. 將黑色物件指向的所有物件都標記成灰色,保證該物件和被該物件引用的物件都不會被回收;
  3. 重複上述兩個步驟直到物件圖中不存在灰色物件;

下圖是準備標記:
https://cdn.learnku.com/uploads/images/202210/05/25530/dxMPCC3E6E.png!large

下圖為當有新物件生成時,因為開啟了寫屏障,會直接標記為黑色
https://cdn.learnku.com/uploads/images/202210/05/25530/mF7msisI9M.png!large

下圖為根物件可達的物件都標記為黑色
https://cdn.learnku.com/uploads/images/202210/05/25530/M0d2JOqxa0.png!large

3、清理階段

然後將灰色物件排入佇列以變為黑色,這表明它們仍在使用中。 一旦所有灰色物體都變成黑色,回收器將再次stop the world並清理所有不再需要的白色節點。 然後應用程式現在可以繼續執行,直到它需要再次清理更多記憶體。

下圖為STW然後清理白色物件
https://cdn.learnku.com/uploads/images/202210/05/25530/A8IBrw08ek.png!large

下圖為清理之後,恢復程式執行
https://cdn.learnku.com/uploads/images/202210/05/25530/tuDiHWQZbU.png!large

一旦程式分配了與正在使用的記憶體成比例的額外記憶體,此過程將再次啟動。 GOGC 環境變數決定了這一點,預設設定為 100。 Go 原始碼將其描述為:

如果 GOGC=100 並且我們使用 4M,我們將在達到 8M 時再次進行 GC(此標記在 next_gc 變數中跟蹤)。 這使 GC 成本與分配成本成線性比例。 調整 GOGC 只會改變線性常數(以及使用的額外記憶體量)。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章