1. 程式人生 > >簡單易懂的 Go 記憶體分配原理解讀

簡單易懂的 Go 記憶體分配原理解讀

1. 前言

編寫過C語言程式的肯定知道通過malloc()方法動態申請記憶體,其中記憶體分配器使用的是glibc提供的ptmalloc2。
除了glibc,業界比較出名的記憶體分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免記憶體碎片和效能上均比glic有比較大的優勢,在多執行緒環境中效果更明顯。

Golang中也實現了記憶體分配器,原理與tcmalloc類似,簡單的說就是維護一塊大的全域性記憶體,每個執行緒(Golang中為P)維護一塊小的私有記憶體,私有記憶體不足再從全域性申請。

另外,記憶體分配與GC(垃圾回收)關係密切,所以瞭解GC前有必要了解記憶體分配的原理。

2. 基礎概念

為了方便自主管理記憶體,做法便是先向系統申請一塊記憶體,然後將記憶體切割成小塊,通過一定的記憶體分配演算法管理記憶體。
以64位系統為例,Golang程式啟動時會向系統申請的記憶體如下圖所示:

預申請的記憶體劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區,應用中需要的記憶體從這裡分配。其中spans和bitmap是為了管理arena區而存在的。

arena的大小為512G,為了方便管理把arena區域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;

spans區域存放span的指標,每個指標對應一個page,所以span區域的大小為(512GB/8KB)*指標大小8byte = 512M

bitmap區域大小也是通過arena計算出來,不過主要用於GC。

2.1 span

span是用於管理arena頁的關鍵資料結構,每個span中包含1個或多個連續頁,為了滿足小物件分配,span中的一頁會劃分更小的粒度,而對於大物件比如超過頁大小,則通過多頁實現。

2.1.1 class

跟據物件大小,劃分了一系列class,每個class都代表一個固定大小的物件,以及每個span的大小。如下表所示:

// class  bytes/obj  bytes/span  objects  waste bytes
//     1          8        8192     1024            0
//     2         16        8192      512            0
//     3         32        8192      256            0
//     4         48        8192      170           32
//     5         64        8192      128            0
//     6         80        8192      102           32
//     7         96        8192       85           32
//     8        112        8192       73           16
//     9        128        8192       64            0
//    10        144        8192       56          128
//    11        160        8192       51           32
//    12        176        8192       46           96
//    13        192        8192       42          128
//    14        208        8192       39           80
//    15        224        8192       36          128
//    16        240        8192       34           32
//    17        256        8192       32            0
//    18        288        8192       28          128
//    19        320        8192       25          192
//    20        352        8192       23           96
//    21        384        8192       21          128
//    22        416        8192       19          288
//    23        448        8192       18          128
//    24        480        8192       17           32
//    25        512        8192       16            0
//    26        576        8192       14          128
//    27        640        8192       12          512
//    28        704        8192       11          448
//    29        768        8192       10          512
//    30        896        8192        9          128
//    31       1024        8192        8            0
//    32       1152        8192        7          128
//    33       1280        8192        6          512
//    34       1408       16384       11          896
//    35       1536        8192        5          512
//    36       1792       16384        9          256
//    37       2048        8192        4            0
//    38       2304       16384        7          256
//    39       2688        8192        3          128
//    40       3072       24576        8            0
//    41       3200       16384        5          384
//    42       3456       24576        7          384
//    43       4096        8192        2            0
//    44       4864       24576        5          256
//    45       5376       16384        3          256
//    46       6144       24576        4            0
//    47       6528       32768        5          128
//    48       6784       40960        6          256
//    49       6912       49152        7          768
//    50       8192        8192        1            0
//    51       9472       57344        6          512
//    52       9728       49152        5          512
//    53      10240       40960        4            0
//    54      10880       32768        3          128
//    55      12288       24576        2            0
//    56      13568       40960        3          256
//    57      14336       57344        4            0
//    58      16384       16384        1            0
//    59      18432       73728        4            0
//    60      19072       57344        3          128
//    61      20480       40960        2            0
//    62      21760       65536        3          256
//    63      24576       24576        1            0
//    64      27264       81920        3          128
//    65      28672       57344        2            0
//    66      32768       32768        1            0

上表中每列含義如下:

  • class: class ID,每個span結構中都有一個class ID, 表示該span可處理的物件型別
  • bytes/obj:該class代表物件的位元組數
  • bytes/span:每個span佔用堆的位元組數,也即頁數*頁大小
  • objects: 每個span可分配的物件個數,也即(bytes/spans)/(bytes/obj)
  • waste bytes: 每個span產生的記憶體碎片,也即(bytes/spans)%(bytes/obj)

上表可見最大的物件是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個物件。

2.1.2 span資料結構

span是記憶體管理的基本單位,每個span用於管理特定的class物件, 跟據物件大小,span將一個或多個頁拆分成多個塊進行管理。

src/runtime/mheap.go:mspan定義了其資料結構:

type mspan struct {
	next *mspan			//連結串列前向指標,用於將span連結起來
	prev *mspan			//連結串列前向指標,用於將span連結起來
	startAddr uintptr // 起始地址,也即所管理頁的地址
	npages    uintptr // 管理的頁數
	
	nelems uintptr // 塊個數,也即有多少個塊可供分配

	allocBits  *gcBits //分配點陣圖,每一位代表一個塊是否已分配

	allocCount  uint16     // 已分配塊的個數
	spanclass   spanClass  // class表中的class ID

	elemsize    uintptr    // class表中的物件大小,也即塊大小
}

以class 10為例,span和管理的記憶體如下圖所示: 

spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個位圖,每位代表一個塊是否被分配,本例中有兩個塊已經被分配,其allocCount也為2。

next和prev用於將多個span連結起來,這有利於管理多個span,接下來會進行說明。

2.2 cache

有了管理記憶體的基本單位span,還要有個資料結構來管理span,這個資料結構叫mcentral,各執行緒需要記憶體時從mcentral管理的span中申請記憶體,為了避免多執行緒申請記憶體時不斷的加鎖,Golang為每個執行緒分配了span的快取,這個快取即是cache。

src/runtime/mcache.go:mcache定義了cache的資料結構:

type mcache struct {
	alloc [67*2]*mspan // 按class分組的mspan列表
}

alloc為mspan的指標陣列,陣列大小為class總數的2倍。陣列中每個元素代表了一種class型別的span列表,每種class型別都有兩組span列表,第一組列表中所表示的物件中包含了指標,第二組列表中所表示的物件不含有指標,這麼做是為了提高GC掃描效能,對於不包含指標的span列表,沒必要去掃描。

根據物件是否包含指標,將物件分為noscan和scan兩類,其中noscan代表沒有指標,而scan則代表有指標,需要GC進行掃描。

mcache和span的對應關係如下圖所示: 

mchache在初始化時是沒有任何span的,在使用過程中會動態的從central中獲取並快取下來,跟據使用情況,每種class的span個數也不相同。上圖所示,class 0的span數比class1的要多,說明本執行緒中分配的小物件要多一些。

2.3 central

cache作為執行緒的私有資源為單個執行緒服務,而central則是全域性資源,為多個執行緒服務,當某個執行緒記憶體不足時會向central申請,當某個執行緒釋放記憶體時又會回收進central。

src/runtime/mcentral.go:mcentral定義了central資料結構:

type mcentral struct {
	lock      mutex     //互斥鎖
	spanclass spanClass // span class ID
	nonempty  mSpanList // non-empty 指還有空閒塊的span列表
	empty     mSpanList // 指沒有空閒塊的span列表

	nmalloc uint64      // 已累計分配的物件個數
}
  • lock: 執行緒間互斥鎖,防止多執行緒讀寫衝突
  • spanclass : 每個mcentral管理著一組有相同class的span列表
  • nonempty: 指還有記憶體可用的span列表
  • empty: 指沒有記憶體可用的span列表
  • nmalloc: 指累計分配的物件個數

執行緒從central獲取span步驟如下:

  1. 加鎖
  2. 從nonempty列表獲取一個可用span,並將其從連結串列中刪除
  3. 將取出的span放入empty連結串列
  4. 將span返回給執行緒
  5. 解鎖
  6. 執行緒將該span快取進cache

執行緒將span歸還步驟如下:

  1. 加鎖
  2. 將span從empty列表刪除
  3. 將span加入noneempty列表
  4. 解鎖

上述執行緒從central中獲取span和歸還span只是簡單流程,為簡單起見,並未對具體細節展開。

2.4 heap

從mcentral資料結構可見,每個mcentral物件只管理特定的class規格的span。事實上每種class都會對應一個mcentral,這個mcentral的集合存放於mheap資料結構中。

src/runtime/mheap.go:mheap定義了heap的資料結構:

type mheap struct {
	lock      mutex

	spans []*mspan

	bitmap        uintptr 	//指向bitmap首地址,bitmap是從高地址向低地址增長的

	arena_start uintptr		//指示arena區首地址
	arena_used  uintptr		//指示arena區已使用地址位置

	central [67*2]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
	}
}
  • lock: 互斥鎖
  • spans: 指向spans區域,用於對映span和page的關係
  • bitmap:bitmap的起始地址
  • arena_start: arena區域首地址
  • arena_used: 當前arena已使用區域的最大地址
  • central: 每種class對應的兩個mcentral

從資料結構可見,mheap管理著全部的記憶體,事實上Golang就是通過一個mheap型別的全域性變數進行記憶體管理的。

mheap記憶體管理示意圖如下: 

系統預分配的記憶體分為spans、bitmap、arean三個區域,通過mheap管理起來。接下來看記憶體分配過程。

3. 記憶體分配過程

針對待分配物件的大小不同有不同的分配邏輯:

  • (0, 16B) 且不包含指標的物件: Tiny分配
  • (0, 16B) 包含指標的物件:正常分配
  • [16B, 32KB] : 正常分配
  • (32KB, -) : 大物件分配 其中Tiny分配和大物件分配都屬於記憶體管理的優化範疇,這裡暫時僅關注一般的分配方法。

以申請size為n的記憶體為例,分配步驟如下:

  1. 獲取當前執行緒的私有快取mcache
  2. 跟據size計算出適合的class的ID
  3. 從mcache的alloc[class]連結串列中查詢可用的span
  4. 如果mcache沒有可用的span則從mcentral申請一個新的span加入mcache中
  5. 如果mcentral中也沒有可用的span則從mheap中申請一個新的span加入mcentral
  6. 從該span中獲取到空閒物件地址並返回

4. 總結

Golang記憶體分配是個相當複雜的過程,其中還摻雜了GC的處理,這裡僅僅對其關鍵資料結構進行了說明,瞭解其原理而又不至於深陷實現細節。

  1. Golang程式啟動時申請一大塊記憶體,並劃分成spans、bitmap、arena區域
  2. arena區域按頁劃分成一個個小塊
  3. span管理一個或多個頁
  4. mcentral管理多個span供執行緒申請使用
  5. mcache作為執行緒私有資源,資源來源於mcentral

本文來自雲棲社群合作伙伴“開源中國” 本文作者:達爾文 原文連結