
本文基于Go1.13
當不再使用內存時,標準庫會自動執行Go的內存管理即從分配到回收。盡管開發者不需要處理它,但是Go的底層管理進行了很好的優化并且充滿了有趣的概念。
堆上的分配
內存管理被設計可以在并發環境快速執行并且集成了gc。讓我們從一個例子開始:
package maintype smallStruct struct {a, b int64c, d float64
}func main() {smallAllocation()
}//go:noinline
func smallAllocation() *smallStruct {return &smallStruct{}
}
注釋//go:noinline
將會阻止內聯優化,以避免內聯通過移除函數的方式優化這段代碼,從而造成最終沒有分配內存的情況。
運行逃逸分析命令go tool compile "-m" main.go
可以確認Go執行了分配:
main.go:14:9: &smallStruct literal escapes to heap
借助go tool compile -S main.go
輸出的程序匯編代碼,同樣可以明確的展示了分配:
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)
函數newobject
是新分配對象和代理mallocgc
的內置函數,該函數在堆上管理它們。Go中有兩種策略,一種用于較小的分配,一種用于較大的分配。
小分配
對于低于32kb的小分配,Go將會嘗試從本地mcache
緩存中獲取內存。此緩存包含一組mspan

每個M
被分配給一個處理器P
并且一次只能處理一個goroutine。當需要分配內存時,當前goroutine會使用它當前P
的本地緩存來從中尋找第一個可用空閑對象。使用本地緩存不需要加鎖會使得分配更加高效。
mspan被分為約70個尺寸類型,從8字節到32k字節。

每個mspan會存在2次:一個不包含指針,一個包含指針。這種區別會使得gc更加容易因為它不需要掃描那些不包含指針的mspan。
在我們之前的例子里,結構體是32字節所以它適合于32 字節的mspan。

現在會疑惑如果mspan在內存分配時候沒有空閑插槽會發生什么。Go維護了包含全尺寸類型的中央鏈表mcentral
,其中包含空閑和非空閑對象的mspan:

mcentral
維護著mspan的雙向鏈表; 在非空鏈表(non-empty list:尚有空閑object的mspan鏈表) — 非空(“non-empty” )代表鏈表中至少有一個插槽是空閑可供分配 — 可能包含一些正在使用的內存。當gc 清理內存時,他會清理一部分mspan標記不再使用,并放回非空鏈表(non-empty list)
我們程序可以在插槽耗盡后向中央鏈表申請mspan:

如果空鏈表中沒有可用的mspan,Go需要為中央鏈表獲取新的mspan。新的mspan會從堆上分配并鏈接到中央鏈表上:

堆在需要時從OS中提取內存。如果需要更多內存,堆會分配一個叫做 arena
的大塊內存, 在 64 位架構下為 64Mb,在其他架構下大多為 4Mb。arena
同樣使用mspan來映射內存:

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

全景圖
現在我們對內存分配的時候發生了什么有了更好的認識。現在將所有的組成部分放在一起來得到全景圖:

編譯整理自 Go: Memory Management and Allocationhttps://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44
