Categories
程式開發

白話Go語言內存管理–內存分配原理


現代高級編程語言管理內存的方式分為兩種:自動和手動,像C、C++ 等編程語言使用手動管理內存的方式,工程師編寫代碼過程中需要主動申請或者釋放內存;而PHP、Java 和Go 等語言使用自動的內存管理系統,有內存分配器和垃圾收集器來代為分配和回收內存。

這篇文章會結合圖示用每個人都能聽懂的大白話解釋清楚Go的內存分配原理。

關於Go的內存分配

在Go語言裡,從內存的分配到不再使用後內存的回收等等這些內存管理工作都是由Go在底層完成的。雖然開發者在寫代碼時不必過度關心內存從分配到回收這個過程,但是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對該函數進行內聯,這樣main函數就會使用smallAllocation函數返回的指針變量,因為被多個函數使用,返回的這個變量將被分配到堆上。

關於內聯的概念之前的文章有說過:

內聯是一種手動或編譯器優化,用於將簡短函數的調用替換為函數體本身。這麼做的原因是它可以消除函數調用本身的開銷,也使得編譯器能更高效地執行其他的優化策略。

所以如果上面的例子不干預編譯器的話,編譯器通過內聯將smallAllocation函數體裡的內容直接放到main函數里,這樣就不會產生smallAllocation這個函數的調用了,所有的變量都是main函數內這個範圍使用的,也就不在需要將變量往堆上分配了。

繼續說上面那個例子,通過逃逸分析命令go tool compile -m main.go 可以確認我們上面的分析,&smallStruct{}會被分配到堆上去。

➜ go tool compile -m main.go
main.go:12:6: can inline main
main.go:10:9: &smallStruct literal escapes to heap

借助命令go tool compile -S main.go,可以顯示該程序的彙編代碼,也可以明確地向我們展示內存的分配:

0x001d 00029 (main.go:10) LEAQ type."".smallStruct(SB), AX
0x0024 00036 (main.go:10) PCDATA $2, $0
0x0024 00036 (main.go:10) MOVQ AX, (SP)
0x0028 00040 (main.go:10) CALL runtime.newobject(SB)

內置函數newobject會通過調用另外一個內置函數mallocgc在堆上分配新內存。在Go裡面有兩種內存分配策略,一種適用於程序裡小內存塊的申請,另一種適用於大內存塊的申請,大內存塊指的是大於32KB。

下面我們來細聊一下這兩種策略。

小於32KB內存塊的分配策略

當程序裡發生了32kb以下的小塊內存申請時,Go會從一個叫做的mcache的本地緩存給程序分配內存。這個本地緩存mcache持有一系列的大小為32kb的內存塊,這樣的一個內存塊裡叫做mspan,它是要給程序分配內存時的分配單元。

白話Go語言內存管理--內存分配原理 1

在Go的調度器模型裡,每個線程M會綁定給一個處理器P,在單一粒度的時間裡只能做多處理運行一個goroutine,每個P都會綁定一個上面說的本地緩存mcache。當需要進行內存分配時,當前運行的goroutine會從mcache中查找可用的mspan。從本地mcache里分配內存時不需要加鎖,這種分配策略效率更高。

那麼有人就會問了,有的變量很小就是數字,有的卻是一個複雜的結構體,申請內存時都分給他們一個mspan這樣的單元會不會產生浪費。其實mcache持有的這一系列的mspan並不都是統一大小的,而是按照大小,從8字節到32KB分了大概70類的msapn。

白話Go語言內存管理--內存分配原理 2

就文章開始的那個例子來說,那個結構體的大小是32字節,正好32字節的這種mspan能滿足需求,那麼分配內存的時候就會給它分配一個32字節大小的mspan。

白話Go語言內存管理--內存分配原理 3

現在,我們可能會好奇,如果分配內存時mcachce裡沒有空閒的32字節的mspan了該怎麼辦? Go裡還為每種類別的mspan維護著一個mcentral。

mcentral的作用是為所有mcache提供切分好的mspan資源。每個central會持有一種特定大小的全局mspan列表,包括已分配出去的和未分配出去的。每個mcentral對應一種mspan,當工作線程的mcache中沒有合適(也就是特定大小的)的mspan時就會從mcentral 去獲取。 mcentral被所有的工作線程共同享有,存在多個goroutine競爭的情況,因此從mcentral獲取資源時需要加鎖。

mcentral的定義如下:

//runtime/mcentral.go

type mcentral struct {
// 互斥锁
lock mutex

// 规格
sizeclass int32

// 尚有空闲object的mspan链表
nonempty mSpanList

// 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
empty mSpanList

// 已累计分配的对象个数
nmalloc uint64
}

mcentral里維護著兩個雙向鍊錶,nonempty表示鍊錶裡還有空閒的mspan待分配。 **empty**表示這條鍊錶裡的mspan都被分配了object。

白話Go語言內存管理--內存分配原理 4

如果上面我們那個程序申請內存的時候,mcache裡已經沒有合適的空閒mspan了,那麼工作線程就會像下圖這樣去mcentral裡去申請。

簡單說下mcache從mcentral獲取和歸還mspan的流程:

獲取加鎖;從nonempty鍊錶找到一個可用的mspan;並將其從nonempty鍊錶刪除;將取出的mspan加入到empty鍊錶;將mspan返回給工作線程;解鎖。歸還加鎖;將mspan從empty鍊錶刪除;將mspan加入到nonempty鍊錶;解鎖。

白話Go語言內存管理--內存分配原理 5

當mcentral沒有空閒的mspan時,會向mheap申請。而mheap沒有資源時,會向操作系統申請新內存。 mheap主要用於大對象的內存分配,以及管理未切割的mspan,用於給mcentral切割成小對象。

白話Go語言內存管理--內存分配原理 6

同時我們也看到,mheap中含有所有規格的mcentral,所以,當一個mcache從mcentral申請mspan時,只需要在獨立的mcentral中使用鎖,並不會影響申請其他規格的mspan。

上面說了每種尺寸的mspan都有一個全局的列表存放在mcentral裡供所有線程使用,所有mcentral的集合則是存放於mheap中的。 mheap裡的arena 區域是真正的堆區,運行時會將8KB 看做一頁,這些內存頁中存儲了所有在堆上初始化的對象。運行時使用二維的 runtime.heapArena” 數組管理所有的內存,每個runtime.heapArena 都會管理64MB 的內存。

白話Go語言內存管理--內存分配原理 7

如果arena 區域沒有足夠的空間,會調用 runtime.mheap.sysAlloc” 從操作系統中申請更多的內存。

大於32KB內存塊的分配策略

Go沒法使用工作線程的本地緩存mcache和全局中心緩存mcentral上管理超過32KB的內存分配,所以對於那些超過32KB的內存申請,會直接從堆上(mheap)上分配對應的數量的內存頁(每頁大小是8KB)給程序。

白話Go語言內存管理--內存分配原理 8

總結

我們把內存分配管理涉及的所有概念串起來,可以勾畫出Go內存管理的一個全局視圖:

白話Go語言內存管理--內存分配原理 9

Go語言的內存分配非常複雜,這個文章從一個比較粗的角度來看Go的內存分配,並沒有深入細節。一般而言,了解它的原理,到這個程度也就可以了(應付面試)。

總結起來關於Go內存分配管理的策略有如下幾點:

Go在程序啟動時,會向操作系統申請一大塊內存,由mheap結構全局管理。 Go內存管理的基本單元是mspan,每種mspan可以分配特定大小的object。 mcache, mcentral, mheap是Go內存管理的三大組件,mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動態分配內存。一般小對象通過mspan分配內存;大對象則直接由mheap分配內存。

相關閱讀

Go內存管理之代碼的逃逸分析

上週並發題的解題思路以及介紹Go語言調度器

參考鏈接

內存管理和分配

圖解Go語言內存分配

內存分配器

感謝你閱讀我的文章,微信搜索公眾號「網管叨bi叨」第一時間獲取更多原創圖解Go語言原理的文章。