Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee F
?? 這篇文章基于 Go 1.13。
在內(nèi)存從分配到回收的生命周期中,內(nèi)存不再被使用的時(shí)候,標(biāo)準(zhǔn)庫(kù)會(huì)自動(dòng)執(zhí)行 Go 的內(nèi)存管理。雖然開(kāi)發(fā)者不必操心這些細(xì)節(jié),但是 Go 語(yǔ)言所做的底層管理經(jīng)過(guò)了很好的優(yōu)化,同時(shí)有很多有趣的概念。
堆上的分配
內(nèi)存管理被設(shè)計(jì)為可以在并發(fā)環(huán)境快速執(zhí)行,同時(shí)與垃圾收集器集成在了一起。從一個(gè)簡(jiǎn)單的例子開(kāi)始:
package main
type smallStruct struct {
a, b int64
c, d float64
}func main() {
smallAllocation()}//go:noinline
func smallAllocation() *smallStruct {
return &smallStruct{}
}
注釋 //go:noinline 會(huì)禁用內(nèi)聯(lián),以避免內(nèi)聯(lián)通過(guò)移除函數(shù)的方式優(yōu)化這段代碼,從而造成最終沒(méi)有分配內(nèi)存的情況出現(xiàn)。
通過(guò)運(yù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 是用于新對(duì)象的分配以及代理 mallocgc 的內(nèi)置函數(shù),該函數(shù)在堆上管理這些內(nèi)存。在 Go 語(yǔ)言中有兩種策略,一種用于較小的內(nèi)存空間的分配,而另一種則用于較大的內(nèi)存空間的分配。
較小內(nèi)存空間的分配策略
對(duì)于小于 32kb 的,較小的內(nèi)存空間的分配策略,Go 會(huì)從被叫做 mcache 的本地緩存中嘗試獲取內(nèi)存。這個(gè)緩存持有一個(gè)被叫做 mspan 的內(nèi)存塊(span ,32kb 大小的內(nèi)存塊)列表, mspan 包含著可用于分配的內(nèi)存:

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

span 的大小等級(jí)
每個(gè) span 鏈表會(huì)存在兩份:一個(gè)鏈表用于不包含指針的對(duì)象而另一個(gè)用于包含指針的對(duì)象。這種區(qū)別使得垃圾收集器更加輕松,因?yàn)樗槐貟呙璨话魏沃羔樀?span。
在我們前面的例子中,結(jié)構(gòu)體的大小是 32 字節(jié),因此它會(huì)適合于 32 字節(jié)的 span :

現(xiàn)在,我們可能會(huì)好奇,如果在分配期間 span 沒(méi)有空閑的插槽會(huì)發(fā)生什么。Go 維護(hù)著每個(gè)大小等級(jí)的 span 的中央鏈表,該中央鏈表被叫做 mcentral,其中維護(hù)著包含空閑對(duì)象的 span 和沒(méi)有空閑對(duì)象的 span :

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

從 mcentral 中替換 span
如果空鏈表中沒(méi)有可用的 span,Go 需要為中央鏈表獲取新的 span 。新的 span 會(huì)從堆上分配,并鏈接到中央鏈表上:

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

堆由 arena 組成
較大內(nèi)存空間的分配策略
Go 并不適用本地緩存來(lái)管理較大的內(nèi)存空間分配。對(duì)于超過(guò) 32kb 的分配,會(huì)向上取整到頁(yè)的大小,并直接從堆上分配。

直接從堆上進(jìn)行大的內(nèi)存空間分配
全景圖
現(xiàn)在我們對(duì)內(nèi)存分配的時(shí)候發(fā)生了什么有了更好的認(rèn)識(shí)。現(xiàn)在將所有的組成部分放在一起來(lái)得到完整的圖畫(huà)。

內(nèi)存分配的組成
靈感來(lái)源
該內(nèi)存分配最初基于 TCMalloc,一個(gè) google 創(chuàng)建的,并發(fā)環(huán)境優(yōu)化的內(nèi)存分配器。這個(gè) TCMalloc 的文檔[1]值得閱讀;你會(huì)發(fā)現(xiàn)上面解釋過(guò)的概念。
via: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44
作者:Vincent Blanchon[2]譯者:dust347[3]校對(duì):@unknwon[4]
本文由 GCTT[5] 原創(chuàng)編譯,Go 中文網(wǎng)[6] 榮譽(yù)推出
參考資料
[1]
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/