圖解 Go 內存管理分配

ℹ️ 這篇文章基於 Go 1.13。

在內存從分配到回收的生命週期中,內存不再被使用的時候,標準庫會自動執行 Go 的內存管理。雖然開發者不必操心這些細節,但是 Go 語言所做的底層管理經過了很好的優化,同時有很多有趣的概念。(「Go 語言中文網」提示你,注意版本,因爲不同版本實現可能會有細微差別

堆上的分配

內存管理被設計爲可以在併發環境快速執行,同時與垃圾收集器集成在了一起。從一個簡單的例子開始:

package main

type smallStruct struct {
   a, b int64
   c, 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 的內存塊 (span ,32kb 大小的內存塊) 列表, mspan 包含着可用於分配的內存:

用 mcache 分配內存

每個線程 M 被分配一個處理器 P,並且一次最多處理一個 goroutine。在分配內存時,當前的 Goroutine 會使用它當前的 P 的本地緩存,在 span 鏈表中尋找第一個可用的空閒對象。使用這種本地緩存不需要鎖操作,從而分配效率更高。

span 鏈表被劃分爲 8 字節大小到 32k 字節大小的,約 70 個的大小等級,每個等級可以存儲不同大小的對象。

span 的大小等級

每個 span 鏈表會存在兩份:一個鏈表用於不包含指針的對象而另一個用於包含指針的對象。這種區別使得垃圾收集器更加輕鬆,因爲它不必掃描不包含任何指針的 span。

在我們前面的例子中,結構體的大小是 32 字節,因此它會適合於 32 字節的 span :

現在,我們可能會好奇,如果在分配期間 span 沒有空閒的插槽會發生什麼。Go 維護着每個大小等級的 span 的中央鏈表,該中央鏈表被叫做 mcentral,其中維護着包含空閒對象的 span 和沒有空閒對象的 span :

span 的中央鏈表

mcentral 維護着 span 的雙向鏈表;其中每個鏈表節點有着指向前一個 span 和後一個 span 的引用。非空鏈表中的 span 可能包含着一些正在使用的內存,“非空” 表示在鏈表中至少有一個空閒的插槽可供分配。當垃圾收集器清理內存時,可能會清理一部分 span,將這部分標記爲不再使用,並將其放回非空鏈表。

我們的程序現在可以在沒有插槽的情況下向中央鏈表請求 span :

從 mcentral 中替換 span

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

從堆上分配 span

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

堆由 arena 組成

較大內存空間的分配策略

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

直接從堆上進行大的內存空間分配

全景圖

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

內存分配的組成

靈感來源

該內存分配最初基於 TCMalloc,一個 Google 創建的,併發環境優化的內存分配器。這個 TCMalloc 的文檔 [1] 值得閱讀;你會發現上面解釋過的概念。


via: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44

作者:Vincent Blanchon[2] 譯者:dust347[3] 校對:@unknwon[4]

本文由 GCTT[5] 原創編譯,Go 中文網 [6] 榮譽推出

參考資料

[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 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bMr4lZgf-Fn1oaOSPPSDIQ