[譯] 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是用於申請記憶體的內建函數,newobjectmallocgc的代理,mallocgc是管理堆記憶體的函數。Go分配記憶體有兩種策略:小塊記憶體申請和大塊記憶體申請。

小塊記憶體申請

對於32KB以下的小塊記憶體申請,Go會嘗試從本地快取mcache中獲取記憶體。mcache包含了一系列被稱為mspanspan列表,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列表,同樣也按大小分成多個級別,叫做mcentralmcentral包含兩種鏈表,一張包含空閑記憶體塊,一張包含已使用記憶體塊:

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 版權聲明: 本文歡迎任何形式轉載,轉載時完整保留本聲明資訊(包含原文鏈接、原文出處、原文作者、版權聲明)即可。本文後續所有修改都會第一時間在原始地址更新。