go記憶體管理
golang記憶體管理
前言
golang實現了自己的記憶體管理,在研究切片擴容的時候,有一步roundsize
調整記憶體大小方法,隨後對golang中的記憶體管理深入學習了下,固記錄。
概念
在程式啟動的時候,golang會預先向處理器申請,如下虛擬地址空間(並沒有真正的申請),主要將其規劃給spans、bitmap、arena三部分
+-----------------------------------------------------------+ | spans | bitmap | arena | +-----------------------------------------------------------+
arena:就是說的記憶體堆區,真正分配記憶體的地方,大小為512G,內部由多個page分割,page是記憶體儲存的基本單元(8KB大小)
spans:存放多個*span,span是記憶體管理的基本單元,後文提及。需要確保每個span都能夠指向不同的page位置,所以spans的大小為(512G/8KB)*每個指標大小8B=512M
bitmap:儲存gc相關的點陣圖資訊
object:程式碼中等待分配的具體物件,在arena區域分配記憶體(一個object可能比page大也可能比page小)
策略
golang中的記憶體分配策略主要參考了C語言中的tcmalloc
,將儲存物件(object)分為大小不同的多種,分別進行管理,減少記憶體碎片的問題,在golang中主要劃分為67種類別class
// class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% 7/8 // 2 16 8192 512 0 43.75% 7/16 // 3 32 8192 256 0 46.88% 15/32 // 4 48 8192 170 32 31.52% .... // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // 9 128 8192 64 0 11.72% // 10 144 8192 56 128 11.82% // 11 160 8192 51 32 9.73% // 12 176 8192 46 96 9.59% // 13 192 8192 42 128 9.25% // 14 208 8192 39 80 8.12% // 15 224 8192 36 128 8.15% // 16 240 8192 34 32 6.62% // 17 256 8192 32 0 5.86% // 18 288 8192 28 128 12.16% // 19 320 8192 25 192 11.80% // 20 352 8192 23 96 9.88% // 21 384 8192 21 128 9.51% // 22 416 8192 19 288 10.71% // 23 448 8192 18 128 8.37% // 24 480 8192 17 32 6.82% // 25 512 8192 16 0 6.05% // 26 576 8192 14 128 12.33% // 27 640 8192 12 512 15.48% // 28 704 8192 11 448 13.93% // 29 768 8192 10 512 13.94% // 30 896 8192 9 128 15.52% // 31 1024 8192 8 0 12.40% // 32 1152 8192 7 128 12.41% // 33 1280 8192 6 512 15.55% // 34 1408 16384 11 896 14.00% // 35 1536 8192 5 512 14.00% // 36 1792 16384 9 256 15.57% // 37 2048 8192 4 0 12.45% // 38 2304 16384 7 256 12.46% // 39 2688 8192 3 128 15.59% // 40 3072 24576 8 0 12.47% // 41 3200 16384 5 384 6.22% // 42 3456 24576 7 384 8.83% // 43 4096 8192 2 0 15.60% // 44 4864 24576 5 256 16.65% // 45 5376 16384 3 256 10.92% // 46 6144 24576 4 0 12.48% // 47 6528 32768 5 128 6.23% // 48 6784 40960 6 256 4.36% // 49 6912 49152 7 768 3.37% // 50 8192 8192 1 0 15.61% // 51 9472 57344 6 512 14.28% // 52 9728 49152 5 512 3.64% // 53 10240 40960 4 0 4.99% // 54 10880 32768 3 128 6.24% // 55 12288 24576 2 0 11.45% // 56 13568 40960 3 256 9.99% // 57 14336 57344 4 0 5.35% // 58 16384 16384 1 0 12.49% // 59 18432 73728 4 0 11.11% // 60 19072 57344 3 128 3.57% // 61 20480 40960 2 0 6.87% // 62 21760 65536 3 256 6.25% // 63 24576 24576 1 0 11.45% // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50%
class:類別
bytes/obj:該class類別每個物件的大小
bytes/span:該class類別span的總大小
objects:可以存放的物件數 (bytes/span)/(bytes/obj)
max waste:最大浪費空間,example:大小為1B的物件為其分配8B的記憶體空間,浪費7/8=87.5%
bytes/obj滿足8*2n的關係,至於為什麼是這個關係?設計考慮到整體max waste需小於某個值,在golang中這個值為多少沒有具體瞭解
span
span是記憶體管理的基本單元,由1個或者多個page組成,page個數在go中也是寫死的
// runtime/sizeclasses.go
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}
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}
// runtime/mheap.go
type mspan struct {
next *mspan
prev *mspan
list *mSpanList
startAddr uintptr // mspan在areana中的開始地址
npages uintptr // span的page數
freeindex uintptr // 下一個可使用object的索引
nelems uintptr // span中object的個數
spanclass spanClass // class
elemsize uintptr // class表中的物件大小,也即塊大小
allocBits uintptr //分配點陣圖,每一位代表一個塊是否已分配
allocCount uint16 // 已分配塊的個數
}
span在原始碼中的資料結構對應mspan,只寫了一部分比較重要的欄位,後文原始碼展示同理。
可以看出mspan是一個雙向的連結串列,儲存其在arena中開始位置startAddr
、分配頁數npages
、span中存放的object資訊等…
spans中*span指標就指向該結構體
基本單元
記憶體管理主要通過mcache
、mcentral
、mheap
進行配合
mcache
// runtime/mcache.go
type mcache struct {
// 每種類別是一個連結串列
// class0 scan class0 noscan
// | |
// | |
// class0 scan class0 noscan
// 每一個mcache有134種mspan(67個指標供指標)
// 每一個mspan可以分配多個object
alloc [67*2]*mspan
}
mcache
就是管理mspan
的基本單元,alloc
欄位為長度67*2的的*mspan陣列,大小為classsize的兩倍,原因是go在處理記憶體時將指標物件和非指標物件區分處理了,對於包含指標的物件需要進行掃描操作scan,不包含指標的物件不需要進行多餘的掃描操作noscan,也是一種效能優化方案。每個class對應一個span連結串列,需要注意的是該連結串列對應的都是該class的span,如註釋中那樣。
在程式執行時每個執行緒P會與一個mcache進行繫結(GMP排程模型),所以mcache
不存在鎖問題,在P執行初始是不存在任何span的
mcentral
mcentral
是記憶體分配的中轉站(central),每個class對應一個mcentral。當某個mcache
無記憶體可用時到相應class的mcentral
中獲取,所以mcentral
是被多個執行緒共享的,需要鎖
// runtime/mcentral.go
type mcentral struct {
// 多個goroutine共享的拿mspan所以需要鎖
lock sync.Mutex
spanclass spanClass
// 維護兩個雙向連結串列
// 當有mcache申請一個mspan時
// 遍歷連結串列查詢可利用的mspan
// 當沒有mspan可用時,mcentral向mheap申請並返回給mcache
nonempty mSpanList // 還有空閒物件或者已經被mcache快取的mspan
empty mSpanList // 沒有空閒物件的mspan聯表
}
mcentral
維護兩個雙向連結串列nonempty
還有空閒物件的span連結串列和empty
沒有空閒物件的span連結串列。
mheap
mheap.central
欄位儲存67*2個mcentral
,程式執行開始預先分配的bitmap
、spans
、arena
就是通過mheap
進行管理,mheap
作為上層結果管理所有的記憶體空間
// runtime/mheap.go
type mheap struct {
// mheap對所有執行緒共享,固需鎖
lock sync.Mutex
allspans []*mspan // 所有mspan
bitmap uintptr //指向bitmap首地址,bitmap是從高地址向低地址增長的
arena_start uintptr //指示arena區首地址
arena_used uintptr //指示arena區已使用地址位置
// heap由134個mcentral組成,67個無指標mcentral
// 67個有指標mcentral
// 每個class 一個mcentral
central [134]struct {
mcentral mcentral
}
}
每個class類別對應一個mcentral
,一共有67*2(scan和noscan兩種)種class也就是134種mcentral
mheap
對所有執行緒共享固需加鎖
記憶體分配流程
前文提及的物件大小劃分圖種只列出了66種,go種將物件大小劃分為67種,最後一種為class為0的類別,bytes/obj>32KB的物件,此種物件稱為大物件,直接由mheap
進行分配、另外就是小於16B的物件稱為tiny物件,go會使用tinyalloc
分配器進行分配,本文著重講[16B,32KB]的物件的記憶體分配管理
當執行緒需要獲取大小為N的記憶體時:
- 判斷N大小屬於兩個class類別區間,向上取整獲取需為其分配的記憶體空間大小,example需要為bool型別的1B大小分配記憶體空間向上取整為8B(所以會有一部分記憶體的浪費)
- 獲取當前執行緒P繫結的
mcache
- 通過物件大小獲取class ID,在
mcache.alloc[class]
中獲取相應class的span
連結串列,判斷是否還有可用的object空間 - 如果沒有可用的span空間則向相應class的
mcentral
申請span
執行緒從mcentral
獲取span:
mcentral
加鎖mcentral
從nonempty中取出可用span並刪除,empty中新增相應span,如果沒有可利用的span則向mheap
獲取mcentral
返回span
mcache
拿到可利用span則獲取空閒object區域,返回地址
執行緒將span返還給mcentral
過程:
mcentral
加鎖- 從empty中刪除相應span,nonempty中新增相應大小span
mcentral
解鎖
總結
- go程式啟動時預先申請一片虛擬地址空間自行管理
- 將地址空間分為
bitmap
、spans
、arena
三個區域,三片區域都通過mheap
資料結構進行統一管理 spans
儲存指向mspan
的指標,mspan
記憶體管理的基本單位,主要以連結串列方式展現。bitmap
儲存gc相關的點陣圖資訊,比如哪些被gc掃描過,哪些沒有。arena
是就是儲存物件的地址堆區span
由一個或多個頁page
組成,在go中已經被定死了,真正儲存的物件為object
,被分為大小不同的67種mcache
與執行緒繫結,執行緒需要獲取記憶體時首先從mcache
獲取,不存在鎖mcentral
供多個執行緒共享span,當mcache
沒有可用span時從mcentral
獲取
參考資料
[圖解go記憶體分配]:https://juejin.cn/post/6844903795739082760
[Go’s Memory Allocator - Overview]:https://andrestc.com/post/go-memory-allocation-pt1/