原來 9 張圖就可以弄懂 Go 內(nèi)存管理

?? 這篇文章基于 Go 1.13。
在內(nèi)存從分配到回收的生命周期中,內(nèi)存不再被使用的時候,標(biāo)準(zhǔn)庫會自動執(zhí)行 Go 的內(nèi)存管理。雖然開發(fā)者不必操心這些細(xì)節(jié),但是 Go 語言所做的底層管理經(jīng)過了很好的優(yōu)化,同時有很多有趣的概念。
堆上的分配
內(nèi)存管理被設(shè)計為可以在并發(fā)環(huán)境快速執(zhí)行,同時與垃圾收集器集成在了一起。從一個簡單的例子開始:
package?main
type?smallStruct?struct?{
???a,?b?int64
???c,?d?float64
}
func?main()?{
???smallAllocation()
}
//go:noinline
func?smallAllocation()?*smallStruct?{
???return?&smallStruct{}
}
注釋 //go:noinline 會禁用內(nèi)聯(lián),以避免內(nèi)聯(lián)通過移除函數(shù)的方式優(yōu)化這段代碼,從而造成最終沒有分配內(nèi)存的情況出現(xiàn)。
通過運行逃逸分析命令 go tool compile "-m" main.go 可以確認(rèn) Go 執(zhí)行了的分配:
main.go:14:9:?&smallStruct?literal?escapes?to?heap
借助 go tool compile -S main.go 命令得到這段程序的匯編代碼,可以同樣明確地向我們展示具體的分配細(xì)節(jié):
0x001d?00029?(main.go:14)???LEAQ???type."".smallStruct(SB),?AX
0x0024?00036?(main.go:14)??PCDATA?$0,?$0
0x0024?00036?(main.go:14)??MOVQ???AX,?(SP)
0x0028?00040?(main.go:14)??CALL???runtime.newobject(SB)
函數(shù) newobject 是用于新對象的分配以及代理 mallocgc 的內(nèi)置函數(shù),該函數(shù)在堆上管理這些內(nèi)存。在 Go 語言中有兩種策略,一種用于較小的內(nèi)存空間的分配,而另一種則用于較大的內(nèi)存空間的分配。
較小內(nèi)存空間的分配策略
對于小于 32kb 的,較小的內(nèi)存空間的分配策略,Go 會從被叫做 mcache 的本地緩存中嘗試獲取內(nèi)存。這個緩存持有一個被叫做 mspan 的內(nèi)存塊(span ,32kb 大小的內(nèi)存塊)列表, mspan 包含著可用于分配的內(nèi)存:

每個線程 M 被分配一個處理器 P,并且一次最多處理一個 goroutine。在分配內(nèi)存時,當(dāng)前的 goroutine 會使用它當(dāng)前的 P 的本地緩存,在 span 鏈表中尋找第一個可用的空閑對象。使用這種本地緩存不需要鎖操作,從而分配效率更高。
span 鏈表被劃分為 8 字節(jié)大小到 32k 字節(jié)大小的,約 70 個的大小等級,每個等級可以存儲不同大小的對象。

每個 span 鏈表會存在兩份:一個鏈表用于不包含指針的對象而另一個用于包含指針的對象。這種區(qū)別使得垃圾收集器更加輕松,因為它不必掃描不包含任何指針的 span。
在我們前面的例子中,結(jié)構(gòu)體的大小是 32 字節(jié),因此它會適合于 32 字節(jié)的 span :
現(xiàn)在,我們可能會好奇,如果在分配期間 span 沒有空閑的插槽會發(fā)生什么。Go 維護(hù)著每個大小等級的 span 的中央鏈表,該中央鏈表被叫做 mcentral,其中維護(hù)著包含空閑對象的 span 和沒有空閑對象的 span :

mcentral 維護(hù)著 span 的雙向鏈表;其中每個鏈表節(jié)點有著指向前一個 span 和后一個 span 的引用。非空鏈表中的 span 可能包含著一些正在使用的內(nèi)存,“非空”表示在鏈表中至少有一個空閑的插槽可供分配。當(dāng)垃圾收集器清理內(nèi)存時,可能會清理一部分 span,將這部分標(biāo)記為不再使用,并將其放回非空鏈表。
我們的程序現(xiàn)在可以在沒有插槽的情況下向中央鏈表請求 span :
如果空鏈表中沒有可用的 span,Go 需要為中央鏈表獲取新的 span 。新的 span 會從堆上分配,并鏈接到中央鏈表上:

堆會在需要的時候從系統(tǒng)( OS )獲取內(nèi)存,如果需要更多的內(nèi)存,堆會分配一個叫做 arena 的大塊內(nèi)存,在 64 位架構(gòu)下為 64Mb,在其他架構(gòu)下大多為 4Mb。arena 同樣適用 span 映射內(nèi)存。

較大內(nèi)存空間的分配策略
Go 并不適用本地緩存來管理較大的內(nèi)存空間分配。對于超過 32kb 的分配,會向上取整到頁的大小,并直接從堆上分配。

全景圖
現(xiàn)在我們對內(nèi)存分配的時候發(fā)生了什么有了更好的認(rèn)識?,F(xiàn)在將所有的組成部分放在一起來得到完整的圖畫。

靈感來源
該內(nèi)存分配最初基于 TCMalloc,一個 Google 創(chuàng)建的,并發(fā)環(huán)境優(yōu)化的內(nèi)存分配器。這個 TCMalloc 的文檔[1]值得閱讀;你會發(fā)現(xiàn)上面解釋過的概念。
via: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44
作者:Vincent Blanchon[2]譯者:dust347[3]校對:@unknwon[4]
本文由 GCTT[5] 原創(chuàng)編譯,Go 中文網(wǎng)[6] 榮譽推出
參考資料
TCMalloc 的文檔: http://goog-perftools.sourceforge.net/doc/tcmalloc.html
[2]Vincent Blanchon: https://medium.com/@blanchon.vincent
[3]dust347: https://github.com/dust347
[4]@unknwon: https://github.com/unknwon
[5]GCTT: https://github.com/studygolang/GCTT
[6]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀

