0.1、索引
https://waterflow.link/articles/1664943418972
文中提到的垃圾回收算法是基於go1.16之後的,讓我們直接進入正題吧。
1、什麼時候需要垃圾回收?
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 編譯器會通過將變量分配到堆棧中來優化它。 程序運行時:
- testStruct 被定義並放置在堆上的一個可用內存塊中。
- myFunction 在函數執行時被執行並分配一個棧。 testVar1 和 testVar2 都存儲在此堆棧中。
- 當 addTwoNumbers 被調用時,一個新的棧幀被壓入棧中,並帶有兩個函數參數。
- 當 addTwoNumbers 完成執行時,它的結果返回給 myFunction 並且 addTwoNumbers 的堆棧幀從堆棧中彈出,因為它不再需要了。
- 指向 testStruct 的指針被定為到包含它的堆上的位置,並且值字段被更新。
- myFunction 退出並且為其創建的堆棧被清理。 testStruct 的值會一直保留在堆上,直到發生垃圾回收。
testStruct 現在在堆上並且沒有分析,Go 運行時不知道是否仍然需要它。 為此,Go 依賴於垃圾回收器。 垃圾回收器有兩個關鍵部分,一個 mutator 和一個回收器。 回收器執行垃圾收集邏輯並找到應該釋放其內存的對象。 mutator 執行應用程序代碼並將新對象分配給堆。 它還會在程序運行時更新堆上的現有對象,其中包括在不再需要某些對象時使其無法訪問。
2、垃圾回收器的實現
Go 的垃圾收集器是一個非分代併發、三色標記和清除垃圾回收器。 讓我們分解一下這些術語。
什麼是分代:
由於“複製”算法對於存活時間長,大容量的儲存對象需要耗費更多的移動時間,和存在儲存對象的存活時間的差異。需要程序將所擁有的內存空間分成若干分區,並標記為年輕代空間和年老代空間。程序運行所需的存儲對象會先存放在年輕代分區,年輕代分區會較為頻密進行較為激進垃圾回收行為,每次回收完成倖存的存儲對象內的壽命計數器加一。當年輕代分區存儲對象的壽命計數器達到一定閾值或存儲對象的佔用空間超過一定閾值時,則被移動到年老代空間,年老代空間會較少運行垃圾回收行為。一般情況下,還有永久代的空間,用於涉及程序整個運行生命週期的對象存儲,例如運行代碼、數據常量等,該空間通常不進行垃圾回收的操作。 通過分代,存活在侷限域,小容量,壽命短的存儲對象會被快速回收;存活在全局域,大容量,壽命長的存儲對象就較少被回收行為處理干擾。——維基百科
分代垃圾回收器專注於最近分配的對象。 但是,如前所述,編譯器優化允許 Go 編譯器將具有已知生命週期的對象分配給堆棧。 這意味着更少的對象將在堆上,因此更少的對象將被垃圾回收。 這意味着在 Go 中不需要分代垃圾回收器。 因此,Go 使用了非分代垃圾回收器。 併發意味着回收器與 mutator 線程同時運行。 因此,Go 使用非分代併發垃圾回收器。 標記和清除是垃圾回收器的類型,三色是用於實現它的算法。
Go 通過幾個步驟實現了這一點:
1、開啓寫屏障
Go 通過一個名為 stop the world 的進程讓所有 goroutine 到達垃圾回收安全點。 這會暫時停止程序運行並打開寫屏障以維護堆上的數據完整性。 這通過允許 goroutine 和回收器同時運行來實現併發。
想要在併發或者增量的標記算法中保證正確性,我們需要達成以下兩種三色不變性(Tri-color invariant)中的一種:
- 強三色不變性 — 黑色對象不會指向白色對象,只會指向灰色對象或者黑色對象;
- 弱三色不變性 — 黑色對象指向的白色對象必須包含一條從灰色對象經由多個白色對象的可達路徑;
一旦所有的 goroutine 都打開了寫屏障,Go 運行時就會starts the world並讓workers執行垃圾回收工作。
2、標記階段
標記是通過使用三色算法實現的。 當標記開始時,根對象是灰色的,其他對象都是白色的。 根是所有其他堆對象的源對象,並作為運行程序的一部分被實例化。 垃圾回收器通過掃描堆棧、全局變量和堆指針開始標記以瞭解正在使用的內容。 掃描堆棧時,workers 停止 goroutine 並通過從根向下遍歷將所有找到的對象標記為灰色。 掃描完成恢復 goroutine。
三色標記的工作原理:
- 從灰色對象的集合中選擇一個灰色對象並將其標記成黑色;
- 將黑色對象指向的所有對象都標記成灰色,保證該對象和被該對象引用的對象都不會被回收;
- 重複上述兩個步驟直到對象圖中不存在灰色對象;
下圖是準備標記:
下圖為當有新對象生成時,因為開啓了寫屏障,會直接標記為黑色
下圖為根對象可達的對象都標記為黑色
3、清理階段
然後將灰色對象排入隊列以變為黑色,這表明它們仍在使用中。 一旦所有灰色物體都變成黑色,回收器將再次stop the world並清理所有不再需要的白色節點。 然後應用程序現在可以繼續運行,直到它需要再次清理更多內存。
下圖為STW然後清理白色對象
下圖為清理之後,恢復程序運行
一旦程序分配了與正在使用的內存成比例的額外內存,此過程將再次啓動。 GOGC 環境變量決定了這一點,默認設置為 100。 Go 源代碼將其描述為:
如果 GOGC=100 並且我們使用 4M,我們將在達到 8M 時再次進行 GC(此標記在 next_gc 變量中跟蹤)。 這使 GC 成本與分配成本成線性比例。 調整 GOGC 只會改變線性常數(以及使用的額外內存量)。