Go語言内置運作時(就是runtime),抛棄了傳統的記憶體配置設定方式,改為自主管理。這樣可以自主地實作更好的記憶體使用模式,比如記憶體池、預配置設定等等。這樣,不會每次記憶體配置設定都需要進行系統調用。
Golang運作時的記憶體配置設定算法主要源自 Google 為 C 語言開發的
TCMalloc算法
,全稱
Thread-CachingMalloc
。核心思想就是把記憶體分為多級管理,進而降低鎖的粒度。它将可用的堆記憶體采用二級配置設定的方式進行管理:每個線程都會自行維護一個獨立的記憶體池,進行記憶體配置設定時優先從該記憶體池中配置設定,當記憶體池不足時才會向全局記憶體池申請,以避免不同線程對全局記憶體池的頻繁競争。
為了更好的閱讀體驗,手動貼上文章目錄:

基礎概念
Go在程式啟動的時候,會先向作業系統申請一塊記憶體(注意這時還隻是一段虛拟的位址空間,并不會真正地配置設定記憶體),切成小塊後自己進行管理。
申請到的記憶體塊被配置設定了三個區域,在X64上分别是512MB,16GB,512GB大小。
arena區域
就是我們所謂的堆區,Go動态配置設定的記憶體都是在這個區域,它把記憶體分割成
8KB
大小的頁,一些頁組合起來稱為
mspan
。
bitmap區域
辨別
arena
區域哪些位址儲存了對象,并用
4bit
标志位表示對象是否包含指針、
GC
标記資訊。
bitmap
中一個
byte
大小的記憶體對應
arena
區域中4個指針大小(指針大小為 8B )的記憶體,是以
bitmap
區域的大小是
512GB/(4*8B)=16GB
。如下圖:
從上圖其實還可以看到bitmap的高位址部分指向arena區域的低位址部分,也就是說bitmap的位址是由高位址向低位址增長的。
spans區域
存放
mspan
(也就是一些
arena
分割的頁組合起來的記憶體管理基本單元,後文會再講)的指針,每個指針對應一頁,是以
spans
區域的大小就是
512GB/8KB*8B=512MB
。除以8KB是計算
arena
區域的頁數,而最後乘以8是計算
spans
區域所有指針的大小。建立
mspan
的時候,按頁填充對應的
spans
區域,在回收
object
時,根據位址很容易就能找到它所屬的
mspan
。
記憶體管理單元
mspan
:Go中記憶體管理的基本單元,是由一片連續的
8KB
的頁組成的大塊記憶體。注意,這裡的頁和作業系統本身的頁并不是一回事,它一般是作業系統頁大小的幾倍。一句話概括:
mspan
是一個包含起始位址、
mspan
規格、頁的數量等内容的雙端連結清單。
每個
mspan
按照它自身的屬性
SizeClass
的大小分割成若幹個
object
,每個
object
可存儲一個對象。并且會使用一個位圖來标記其尚未使用的
object
。屬性
SizeClass
決定
object
大小,而
mspan
隻會配置設定給和
object
尺寸大小接近的對象,當然,對象的大小要小于
object
大小。還有一個概念:
SpanClass
,它和
SizeClass
的含義差不多,
Size_Class = Span_Class / 2
這是因為其實每個
SizeClass
有兩個
mspan
,也就是有兩個
SpanClass
。其中一個配置設定給含有指針的對象,另一個配置設定給不含有指針的對象。這會給垃圾回收機制帶來利好,之後的文章再談。
如下圖,
mspan
由一組連續的頁組成,按照一定大小劃分成
object
。
Go1.9.2裡
mspan
的
SizeClass
共有67種,每種
mspan
分割的object大小是8*2n的倍數,這個是寫死在代碼裡的:
// path: /usr/local/go/src/runtime/sizeclasses.go
const _NumSizeClasses = 67
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
根據
mspan
的
SizeClass
可以得到它劃分的
object
大小。 比如
SizeClass
等于3,
object
大小就是32B。 32B大小的object可以存儲對象大小範圍在17B~32B的對象。而對于微小對象(小于16B),配置設定器會将其進行合并,将幾個對象配置設定到同一個
object
中。
數組裡最大的數是32768,也就是32KB,超過此大小就是大對象了,它會被特别對待,這個稍後會再介紹。順便提一句,類型
SizeClass
為0表示大對象,它實際上直接由堆記憶體配置設定,而小對象都要通過
mspan
來配置設定。
對于mspan來說,它的
SizeClass
會決定它所能分到的頁數,這也是寫死在代碼裡的:
// path: /usr/local/go/src/runtime/sizeclasses.go
const _NumSizeClasses = 67
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
比如當我們要申請一個
object
大小為
32B
的
mspan
的時候,在classtosize裡對應的索引是3,而索引3在
class_to_allocnpages
數組裡對應的頁數就是1。
mspan
結構體定義:
// path: /usr/local/go/src/runtime/mheap.go
type mspan struct {
//連結清單後向指針,用于将span連結起來
next *mspan
//連結清單前向指針,用于将span連結起來
prev *mspan
// 起始位址,也即所管理頁的位址
startAddr uintptr
// 管理的頁數
npages uintptr
// 塊個數,表示有多少個塊可供配置設定
nelems uintptr
//配置設定位圖,每一位代表一個塊是否已配置設定
allocBits *gcBits
// 已配置設定塊的個數
allocCount uint16
// class表中的class ID,和Size Classs相關
spanclass spanClass
// class表中的對象大小,也即塊大小
elemsize uintptr
}
我們将
mspan
放到更大的視角來看:
上圖可以看到有兩個
S
指向了同一個
mspan
,因為這兩個
S
指向的
P
是同屬一個
mspan
的。是以,通過
arena
上的位址可以快速找到指向它的
S
,通過
S
就能找到
mspan
,回憶一下前面我們說的
mspan
區域的每個指針對應一頁。
假設最左邊第一個
mspan
的
SizeClass
等于10,根據前面的
class_to_size
數組,得出這個
msapn
分割的
object
大小是144B,算出可配置設定的對象個數是
8KB/144B=56.89
個,取整56個,是以會有一些記憶體浪費掉了,Go的源碼裡有所有
SizeClass
的
mspan
浪費的記憶體的大小;再根據
class_to_allocnpages
數組,得到這個
mspan
隻由1個
page
組成;假設這個
mspan
是配置設定給無指針對象的,那麼
spanClass
等于20。
startAddr
直接指向
arena
區域的某個位置,表示這個
mspan
的起始位址,
allocBits
指向一個位圖,每位代表一個塊是否被配置設定了對象;
allocCount
則表示總共已配置設定的對象個數。
這樣,左起第一個
mspan
的各個字段參數就如下圖所示:
記憶體管理元件
記憶體配置設定由記憶體配置設定器完成。配置設定器由3種元件構成:
mcache
,
mcentral
,
mheap
。
mcache
mcache
:每個工作線程都會綁定一個mcache,本地緩存可用的
mspan
資源,這樣就可以直接給Goroutine配置設定,因為不存在多個Goroutine競争的情況,是以不會消耗鎖資源。
mcache
的結構體定義:
//path: /usr/local/go/src/runtime/mcache.go
type mcache struct {
alloc [numSpanClasses]*mspan
}
numSpanClasses = _NumSizeClasses << 1
mcache
用
SpanClasses
作為索引管理多個用于配置設定的
mspan
,它包含所有規格的
mspan
。它是
_NumSizeClasses
的2倍,也就是
67*2=134
,為什麼有一個兩倍的關系,前面我們提到過:為了加速之後記憶體回收的速度,數組裡一半的
mspan
中配置設定的對象不包含指針,另一半則包含指針。
對于無指針對象的
mspan
在進行垃圾回收的時候無需進一步掃描它是否引用了其他活躍的對象。 後面的垃圾回收文章會再講到,這次先到這裡。
mcache
在初始化的時候是沒有任何
mspan
資源的,在使用過程中會動态地從
mcentral
申請,之後會緩存下來。當對象小于等于32KB大小時,使用
mcache
的相應規格的
mspan
進行配置設定。
mcentral
mcentral
:為所有
mcache
提供切分好的
mspan
資源。每個
central
儲存一種特定大小的全局
mspan
清單,包括已配置設定出去的和未配置設定出去的。 每個
mcentral
對應一種
mspan
,而
mspan
的種類導緻它分割的
object
大小不同。當工作線程的
mcache
中沒有合适(也就是特定大小的)的
mspan
時就會從
mcentral
擷取。
mcentral
被所有的工作線程共同享有,存在多個Goroutine競争的情況,是以會消耗鎖資源。結構體定義:
//path: /usr/local/go/src/runtime/mcentral.go
type mcentral struct {
// 互斥鎖
lock mutex
// 規格
sizeclass int32
// 尚有空閑object的mspan連結清單
nonempty mSpanList
// 沒有空閑object的mspan連結清單,或者是已被mcache取走的msapn連結清單
empty mSpanList
// 已累計配置設定的對象個數
nmalloc uint64
}
用圖來表示:
empty
表示這條連結清單裡的
mspan
都被配置設定了
object
,或者是已經被
cache
取走了的
mspan
,這個
mspan
就被那個工作線程獨占了。而
nonempty
則表示有空閑對象的
mspan
清單。每個
central
結構體都在
mheap
中維護。
簡單說下
mcache
從
mcentral
擷取和歸還
mspan
的流程:
- 擷取 加鎖;從
連結清單找到一個可用的nonempty
;并将其從mspan
連結清單删除;将取出的nonempty
加入到mspan
連結清單;将empty
傳回給工作線程;解鎖。mspan
- 歸還 加鎖;将
從mspan
連結清單删除;将empty
加入到mspan
連結清單;解鎖。nonempty
mheap
mheap
:代表Go程式持有的所有堆空間,Go程式使用一個
mheap
的全局對象
_mheap
來管理堆記憶體。
當
mcentral
沒有空閑的
mspan
時,會向
mheap
申請。而
mheap
沒有資源時,會向作業系統申請新記憶體。
mheap
主要用于大對象的記憶體配置設定,以及管理未切割的
mspan
,用于給
mcentral
切割成小對象。
同時我們也看到,
mheap
中含有所有規格的
mcentral
,是以,當一個
mcache
從
mcentral
申請
mspan
時,隻需要在獨立的
mcentral
中使用鎖,并不會影響申請其他規格的
mspan
。
mheap
結構體定義:
//path: /usr/local/go/src/runtime/mheap.go
type mheap struct {
lock mutex
// spans: 指向mspans區域,用于映射mspan和page的關系
spans []*mspan
// 指向bitmap首位址,bitmap是從高位址向低位址增長的
bitmap uintptr
// 訓示arena區首位址
arena_start uintptr
// 訓示arena區已使用位址位置
arena_used uintptr
// 訓示arena區末位址
arena_end uintptr
central [67*2]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
}
用圖來表示:
上圖我們看到,bitmap和arena_start指向了同一個位址,這是因為bitmap的位址是從高到低增長的,是以他們指向的記憶體位置相同。
記憶體配置設定流程
上一篇文章《Golang之變量去哪兒》中我們提到了,變量是在棧上配置設定還是在堆上配置設定,是由逃逸分析的結果決定的。通常情況下,編譯器是傾向于将變量配置設定到棧上的,因為它的開銷小,最極端的就是"zero garbage",所有的變量都會在棧上配置設定,這樣就不會存在記憶體碎片,垃圾回收之類的東西。
Go的記憶體配置設定器在配置設定對象時,根據對象的大小,分成三類:小對象(小于等于16B)、一般對象(大于16B,小于等于32KB)、大對象(大于32KB)。
大體上的配置設定流程:
- >32KB 的對象,直接從mheap上配置設定;
- <=16B 的對象使用mcache的tiny配置設定器配置設定;
- (16B,32KB] 的對象,首先計算對象的規格大小,然後使用mcache中相應規格大小的mspan配置設定;
- 如果mcache沒有相應規格大小的mspan,則向mcentral申請
- 如果mcentral沒有相應規格大小的mspan,則向mheap申請
- 如果mheap中也沒有合适大小的mspan,則向作業系統申請
總結
Go語言的記憶體配置設定非常複雜,它的一個原則就是能複用的一定要複用。源碼很難追,後面可能會再來一篇關于記憶體配置設定的源碼閱讀相關的文章。簡單總結一下本文吧。
文章從一個比較粗的角度來看Go的記憶體配置設定,并沒有深入細節。一般而言,了解它的原理,到這個程度也可以了。
- Go在程式啟動時,會向作業系統申請一大塊記憶體,之後自行管理。
- Go記憶體管理的基本單元是mspan,它由若幹個頁組成,每種mspan可以配置設定特定大小的object。
- mcache, mcentral, mheap是Go記憶體管理的三大元件,層層遞進。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動态配置設定記憶體。
- 極小對象會配置設定在一個object中,以節省資源,使用tiny配置設定器配置設定記憶體;一般小對象通過mspan配置設定記憶體;大對象則直接由mheap配置設定記憶體。