Go 內存管理概述
隨着程序的運行,對象被寫入內存。在一些特定時刻當它們不再被需要時,它們應該被移除。這個過程被稱爲 內存管理 。本文旨在給出內存管理的概述,然後深入研究在 Go 中如何使用垃圾收集器實現內存管理。Go 的內存管理近些年已經發生了很大變化,未來很可能還會發生更多變化。如果您正在閱讀這篇文章,並且您使用的是比 1.16 更高的 Go 版本,那麼這裏的一些信息可能已經過時了。
手動內存管理
在像 C 這樣的編程語言中,程序員會調用 malloc
或 calloc
之類的函數來將對象寫入內存。這些函數返回一個指針,指向該對象在堆內存中的位置。當這個對象不再被需要時,程序員調用 free
函數來再釋放以便再次使用這塊內存。這種內存管理的方法被稱爲 顯式釋放 。它非常的強大,使程序員能夠更好地控制正在使用的內存,從而允許某些類型優化變得更加容易,特別是在小內存環境下。但是,它也會導致兩種類型的編程錯誤。
第一種是提前調用 free
,這會創建一個 懸空指針 。懸空指針是指不再指向內存中有效對象的指針。那麼這會非常糟糕,因爲程序期望一個指針指向的是已定義的值,而當這個懸空指針稍後被訪問時,並不能保證在內存中該位置存在什麼值。可能什麼都沒有,或者完全是其他值。第二種錯誤,內存根本無法釋放。如果程序員忘記釋放一個對象,他們可能會面臨 內存泄漏 風險,因爲內存會被越來越多的對象填滿。如果內存不足,這可能導致程序變慢或崩潰。所以當不得不顯式地管理內存時,可能會在程序中引入不可預測的錯誤。
自動內存管理
這是像 Go 這樣的語言提供了 自動的動態內存管理 ,或者更簡單地說,垃圾收集 的原因。具有垃圾收集功能的語言提供瞭如下好處:
-
安全性的提高
-
更好的跨操作系統移植性
-
需要編寫的代碼更少
-
代碼的運行時校驗
-
數組的邊界檢查
確實垃圾收集會帶來性能開銷,但並不像通常認爲的那樣多。所以折衷的方案是,程序員專注於他們程序的業務邏輯,並確保它符合目標,而不用擔心管理內存。
一個正在運行的程序將對象存儲在內存中的兩個位置, 堆 和 棧 。垃圾收集作用於堆上,而不是棧。棧是一個存儲函數值的後進先出數據結構。從函數內部調用另一個函數,會將一個新的 棧幀 放到棧上,它包含被調用函數的值等。當函數調用返回時,它的棧楨將會從棧上彈出。當在調試一個崩潰的程序時,您可能會熟悉棧這一結構。大多數語言的編譯器會返回一個調用棧來幫助跟蹤調試,它會顯示在這一點之前被調用的函數。
棧可以以一種後進先出的方式將值 “推” 到頂部,或者從頂部 “彈出” 。圖片來源 Wikipedia.
與棧相反,堆中包含的是在函數外部被引用的值。例如,在程序開始時定義的靜態常量,或更復雜的對象,如 Go 結構體。當程序員定義一個放置在堆上的對象時,將分配所需的內存大小,並返回指向該對象的指針。堆是一種圖結構,對象代表着節點,這些節點被代碼或者其他對象所引用。隨着程序的運行,堆將隨着對象的添加而繼續增長,除非對堆做清理。
堆從根節點開始,隨着更多的對象被添加而增長。
Go 中的垃圾收集
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() {
// some other code
myFunction()
// some more code
}
出於本例的目的,讓我們假設這是一個正在運行的程序的一部分,因爲如果這是整個程序,那麼 Go 編譯器會通過將變量分配到棧來優化它。當程序運行時:
-
testStruct
是被定義和放置在堆中的一個可用內存塊 -
myFunction
函數被調用執行時將會分配一個棧。testVar1
和testVar2
都被存儲在這個棧上。 -
調用
addTwoNumbers
時,一個新的棧幀被推到棧上,並帶有函數的兩個參數。 -
當
addTwoNumbers
完成執行,它的結果返回給myFunctio
n 並且addTwoNumbers
的棧幀從棧中彈出,因爲它不再被需要。 -
指向
testStruct
的指針被跟隨到它堆上的位置,並且value
字段被更新。 -
myFunction
退出,並清除爲它創建的棧。testStruct
的值繼續保持在堆上,直到垃圾收集發生。
testStruct
現在在堆上,也沒有使用,Go 運行時也不知道是否仍然需要它。爲此,Go 依賴於一個垃圾收集器。垃圾收集器有兩個關鍵部分,一個 更改器 和一個 收集器。收集器執行垃圾收集邏輯並找到應該釋放其內存的對象。更改器執行應用程序代碼並將新對象分配給堆。它還在程序運行時更新堆上的現有對象,包括使不再需要的某些對象變爲不可達。
由於更改器所做的更改,底部的對象已變爲不可訪問。它應該由垃圾收集器清理。
Go 垃圾收集器的實現
Go 的垃圾收集器是一個 非分代,併發,三色標記清除的垃圾收集器。讓我們把這幾項分解。
分代假設 是壽命短的對象(如臨時變量)最常被回收。因此,分代垃圾收集器主要關注最近分配的對象。然而如前所述,編譯器優化允許 Go 編譯器將具有已知生命週期的對象分配在棧上。這意味着堆上的對象更少,因此垃圾收集的對象更少。這也意味着在 Go 中不需要分代垃圾收集器。因此,Go 使用了一個非分代的垃圾收集器。併發意味着收集器與更改器線程同時運行。因此,Go 使用的是一個非分代、併發的垃圾收集器。標記清除是垃圾收集器的工作類型,三色是用於實現這一功能的算法。
一個標記清除垃圾收集器有兩個階段,不出所料地命名爲 標記 和 清除 。在標記階段,收集器遍歷堆並標記不再需要的對象。後續掃描階段將刪除這些對象。標記和清除是一種間接算法,因爲它標記活動對象,並移除其他所有東西。
原圖地址:https://github.com/gocn/translator/raw/master/static/images/w21_An_overview_of_memory_management_in_Go/figure4.gif
可視化的標記清除收集器過程,來源於這裏。如果你感興趣的話,還可以看到其他類型的垃圾收集器。
Go 用幾個步驟實現了這一點:
Go 讓所有的 goroutines 到達一個垃圾收集安全點,並使用一個名爲 stop the world 的過程。這將暫時停止程序的運行,並打開一個 寫屏障 以維護堆上的數據完整性。通過允許 goroutine 和收集器同時運行,從而實現了併發性。
一旦所有的 goroutine 都打開了寫障礙,Go 運行 starts the world 並讓工作線程開始執行垃圾收集工作。
標記是通過使用一個 三色算法 實現的。當標記開始時,除了根對象是灰色的,所有對象都是白色的。根是所有其他堆對象的來源,並作爲運行程序的一部分實例化。垃圾收集器首先掃描棧、全局變量和堆指針,以瞭解什麼對象正在使用。當掃描一個棧時,工作線程將停止 goroutine ,並通過從根向下遍歷將所有發現的對象標記爲灰色。然後繼續執行 goroutine 。
然後,灰色的對象將入隊變成黑色,這表明它們仍在使用中。一旦所有的灰色對象被標爲黑色,收集器將會再一次 stop the world 並且清理所有不再被需要的白色節點對象。程序現在可以繼續運行,直到它需要再次清理更多內存。
這張來自維基百科的圖表讓上述更容易理解。顏色有點混亂,但白色物體是淺灰色,灰色物體是黃色,黑色物體是藍色。
一旦程序按照使用的內存比例分配了額外的內存,這個進程將再次啓動。GOGC 環境變量決定了這一比例,默認值爲 100 。Go 的源代碼描述如下:
如果 GOGC=100 並且我們正在使用 4M 內存,我們將在到達 8M 時再次進行 GC(這個標記在 next_gc 變量中被跟蹤)。這使 GC 成本與分配成本成線性比例。調整 GOGC 只是改變線性常數(還有額外內存的使用量)。
Go 的垃圾收集器通過將內存管理抽象到 Go 運行時來提高效率,這也是使 Go 具有如此優秀性能的原因之一。Go 內置的工具允許您優化程序中垃圾收集的觸發行爲,如果您感興趣,可以對此進行研究。至此,我希望您瞭解到了更多關於垃圾收集的工作原理和在 Go 中如何實現垃圾收集的知識。
參考
-
Garbage Collection in Go: Part 1
-
Getting to Go: The Journey of Go’s Garbage Collector
-
Go: How Does the Garbage Collector Mark the Memory?
-
Golang: Cost of using the heap
-
Golang FAQ
-
Google Groups discussion, comment by Ian Lance Taylor
-
Implementing memory management with Golang’s garbage collector
-
Memory Management Reference
-
Stack (abstract data type)
-
The Garbage Collection Handbook
-
Tracing garbage collection: Tri-color marking
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/PNRhtdS_gZVTtrkkRmx7yA