[譯] Go語言記憶體管理與分配
- 2019 年 11 月 10 日
- 筆記
本文基於Go 1.13
Go程式的記憶體從申請階段到不再使用後的釋放階段都由Go標準庫自動管理。儘管管理工作不需要開發者參與,但是Go對記憶體管理的底層實現做了非常好的優化,裡面充滿了有意思的知識點,還是值得我們學習的。
從堆上申請記憶體
Go記憶體管理的設計目標是在並發環境下保持高性能,並且集成垃圾回收器。讓我們從一個簡單的例子開始:
package maintype smallStruct struct { a, b int64 c, d float64}func main() { smallAllocation()}//go:noinlinefunc smallAllocation() *smallStruct { return &smallStruct{}} |
---|
//go:noinline
這行注釋可以禁止編譯時的內聯優化,從而避免編譯時把smallAllocation
這個函數調用直接優化沒了。
運行逃逸分析命令go tool compile "-m" main.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), AX0x0024 00036 (main.go:14) PCDATA $0, $00×0024 00036 (main.go:14) MOVQ AX, (SP)0x0028 00040 (main.go:14) CALL runtime.newobject(SB) |
---|
newobject
是用於申請記憶體的內建函數,newobject
是mallocgc
的代理,mallocgc
是管理堆記憶體的函數。Go分配記憶體有兩種策略:小塊記憶體申請和大塊記憶體申請。
小塊記憶體申請
對於32KB以下的小塊記憶體申請,Go會嘗試從本地快取mcache
中獲取記憶體。mcache
包含了一系列被稱為mspan
的span
列表,mspan
包含了可供分配使用的記憶體:

Go的執行緒調度模型中,每個系統執行緒M
和一個上下文P
掛鉤,在一個指定時間點最多只能處理一個協程G
。申請記憶體時,當前協程會首先在所屬M
的本地快取中的span
列表中查找可用的記憶體塊。使用本地快取的好處是不用加鎖,更高效。
span
列表按大小被劃分為大約70個等級,大小從8位元組到32K位元組不等,不同等級存儲不同大小的記憶體塊:

在我們前面的例子,結構體的大小為32位元組,所以使用32位元組的span
:

每個等級的span
鏈表會存在兩份:一個鏈表用於存儲內部不包含指針的對象,另一個鏈表用於存儲內部包含指針的對象。這麼的好處是垃圾回收時更高效,因為不需要掃描不包含指針的那個span
鏈表。
譯者 yoko 註: 對英文原文做個補充。 每個
mcache
包含了2 * 67
個鏈表(一個元素個數為2 * 67
的數組,數組中的一個元素即為一個mspan
鏈表)。 這裡的67怎麼來的呢,為什麼不是1呢? 實際上每個mspan
都各自管理了一大塊記憶體塊,而每個mspan
又被切割成n個小記憶體塊(object
),object
才是真正分配給用戶使用的記憶體塊。 那麼問題來了,mspan
按多大切割成object
合適呢,太小可能不滿足用戶申請的大小,太大又造成浪費。 Go採取的策略是將32K大小以內的大小預定義了67個大小等級,每一個鏈表中的所有mspan
都按該鏈表所設定的大小等級切割object
。 這樣,用戶申請記憶體時,向上取最接近的大小等級,然後去對應的鏈表中的mspan
獲取可用的object
。 英文原文關於這部分說的不太清楚,並且上面的兩張圖畫得都不是太準確。實際上應該是一行可能有多個mspan
,然後每個mspan
內又可能包含多個object
。
現在,你可能會奇怪如果mcache
上沒有空閑的記憶體塊可供分配該怎麼辦。Go另外還維護了全局的span
列表,同樣也按大小分成多個級別,叫做mcentral
。mcentral
包含兩種鏈表,一張包含空閑記憶體塊,一張包含已使用記憶體塊:

mcentral
維護了兩張span
鏈表。一張鏈表為non-empty
類型,包含了可供分配的span
(由於一個span
可能包含多個object
,只要有一個或一個以上的object可供分配即表示該span
可供分配),一張為empty
類型,包含已分配完畢的span
。當Go執行垃圾回收時,如果span
中的記憶體塊被標記為可供分配,span
會重新加入到non-empty
鏈表中。
從mcentral
獲取span
的流程圖如下:

當mcentral
中也沒有可供分配的span
時,Go會從堆上申請新的span
並將其放入mcentral
中:

堆在必要時向作業系統申請記憶體。它會申請一塊大記憶體,被稱為arena
,在64位系統下為64MB,其它大部分系統為4MB,申請的記憶體同樣用span
管理:

大塊記憶體申請
Go申請大於32KB的大塊記憶體不使用本地快取策略,而是將大小取整到頁大小整數倍後直接從堆上申請。

全局圖
現在我們在一個較高層次上,對Go的記憶體分配有了一個大致了解。讓我們將所有的組件集合到一起來繪製一張全局圖:

設計靈感
Go記憶體分配器的設計基於TCMalloc,TCMalloc是由Google專門為並行環境優化的記憶體分配器。TCMalloc的文檔很值得一讀,在文檔里你也能找到本文中講解到的一些概念。
原文鏈接: https://pengrl.com/p/38720/ 原文出處: yoko blog (https://pengrl.com) 原文作者: yoko 版權聲明: 本文歡迎任何形式轉載,轉載時完整保留本聲明資訊(包含原文鏈接、原文出處、原文作者、版權聲明)即可。本文後續所有修改都會第一時間在原始地址更新。