概述
? golang的內存分配機制源自Google的tcmalloc算法,英文全稱thread caching malloc,從名字可以看出,是在原有基礎上,針對多核多線程的內存管理進行優化而提出來的。該算法的核心思想是內存的多級管理,進而降低鎖的粒度;將內存按需劃成大小不一的塊,減少內存的碎片化。為每個P,也就是go協程調度模型了里面的邏輯處理器維護一個mcache結構體的獨立內存池,只有當該內存池不足時,才會向全局mcentral和mheap結構體管理的內存池申請。為每一個P維持一個私有的本地內存池,從而不用加鎖,加快了內存分配速度。只有在本地P的內存池被消耗完,或者申請的內存太大時,才會訪問全局的內存池,大大減小了多線程下對全局內存池訪問帶來的競爭系統損耗。
內存架構
? 以64位的系統為例,1.11版本以后go程序的整個堆內存是連續,如下圖 所示。結構雖然簡單,但是在混合使用go和c的時候會導致程序崩潰,例如分配的內存地址發生沖突,導致初始化堆和擴容失敗。
? 自1.11版本以后,對堆實現了分塊處理,arena不再是連續的,以64位的Linux系統為例,是一個塊塊64MB大小的塊。golang內存的三級架構如下圖所示。下面將分別介紹各個層。
mspan
? mspan結構體是go內存管理的基本單元,定義在runtime/mheap.go中,主要結構體成員如下:
//go:notinheap
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
nelems uintptr // number of object in the span.
allocBits *gcBits
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
elemsize uintptr // computed from sizeclass or from npages
.....
}
? 可以發現,這是一個雙向鏈表。
? startAddr:當前span在arena中的起始字節的地址
? npages:當前span包含arena中多少頁
? nelems:當前span,包含多少個對象。golang又對每一個span,按照所屬class的不同,切分成大小不同的塊,以減少內存碎片。
? allocCount:已分配的對象數目
? elemsize:對象大小
? spanclass:span所屬的class。
? 根據對象的大小,golang劃分了一系列的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的種類,可以看到一共66中,實際一共67種。大于32K的內存 分配,會直接從mheap中分配,后面會介紹。
? bytes/obj:每個對象占用的字節數
? bytes/span:每個span的大小,也就是頁數*8k(頁大小)
? objects:該類span所擁有的對象數,span所占字節數/對象所占字節數
? waste bytes:該類span浪費的字節數,從以上分析可以看出,每一類span并不能剛好按該類對象大小,分配整數個對象,即做到每一字節物盡其用,這個值是:span所占字節數%對象所占字節數
? 以class 10 為例,span與管理的內存如下圖所示:
表示當前span類別屬于class10,大小只有1頁,又切分56個大小為144字節的塊,其中兩個已分配。
mheap
? mheap管理整個go程序的堆空間,在源文件runtime/mheap.go中找到了該結構體描述,以及全局變量mheap_,結構體主要字段如下:
type mheap struct {
// lock must only be acquired on the system stack, otherwise a g
// could self-deadlock if its stack grows with the lock held.
lock mutex
pages pageAlloc // page allocation data structure
allspans []*mspan // all spans out there
// Malloc stats.
largealloc uint64 // bytes allocated for large objects
nlargealloc uint64 // number of large object allocations
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
curArena struct {
base, end uintptr
}
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}
? pages:堆的也分配
? allspans:所有分配的span
? largealloc:超過32k大對象的分配空間的字節數
? nlargealloc:超過32k大對象分配的對象數目
? central:mcentral結構單元
? spanalloc:mspan分配器
? cachealloc:mache分配器
? curArena: 當前arena的起始地址
? arenas:將虛擬地址空間以arena幀的形式一片片分割
? arenas變成了一個heapArea的指針數組。
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
zeroedBase uintptr
}
? 這個結構體描繪了一個arena,查看runtime/malloc.go
heapArenaBytes = 1 << logHeapArenaBytes
// logHeapArenaBytes is log_2 of heapArenaBytes. For clarity,
// prefer using heapArenaBytes where possible (we need the
// constant to compute some other constants).
logHeapArenaBytes = (6+20)*(_64bit*(1-sys.GoosWindows)*(1-sys.GoarchWasm)) + (2+20)*(_64bit*sys.GoosWindows) + (2+20)*(1-_64bit) + (2+20)*sys.GoarchWasm
// heapArenaBitmapBytes is the size of each heap arena's bitmap.
heapArenaBitmapBytes = heapArenaBytes / (sys.PtrSize * 8 / 2)
pagesPerArena = heapArenaBytes / pageSize
```
在64位Linux系統上,單個arena的大小heapArenaBytes是64MB,每個arena分成8k大小的頁。整個虛擬內存的第一級切分圖,如下所示:
每個heapArena結構體按下圖管理每個arena,
? bitmap在gc的時候起作用,其中每個字節標識了arena每四個指針大小空間(也就是32字節大小)的內容情況:1位標識是否已掃描,一位標識是否有指針
mcentral
? 從mheap的結構體中,可以看到,mheap創建了一個包含164個mcentral對象的數組。也就是mheap管理著164個mcentral。mcentral結構體類型如下所示:
type mcentral struct {
lock mutex
spanclass spanClass
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
empty mSpanList // list of spans with no free objects (or cached in an mcache)
// nmalloc is the cumulative count of objects allocated from
// this mcentral, assuming all spans in mcaches are
// fully-allocated. Written atomically, read under STW.
nmalloc uint64
}
? lock:互斥鎖
? spanclass:所屬span的類型,從這里可以推斷,應該是每一種類型的span,都有一個對應的mcentral結構體
? nonempty:含有空對象且可分配的span列表,查看這個類型,可以發現是個知頭尾的雙向鏈表
type mSpanList struct {
first *mspan // first span in list, or nil if none
last *mspan // last span in list, or nil if none
}
? empty:不含空對象且不可分配的span列表
? nmalloc:已分配的累計對象數目
? mcentral為所有mcache提供分配好的mspan資源。當某一個P私有的mcache沒有可用的span的以后,會動態的從mcentral申請,之后就會緩存在mcache中。前面介紹到mheap會創建134個mcentral,也就是每個class類型的span會有兩個對應的mcentral:span內包含指針和不包含指針的。
? mcentral與mspan的對應關系如下圖所示:
? 先簡單總結mcache從mcentral獲取和歸還span:
? 獲取:先加鎖,從nonempty鏈表找一個可用的mspan,從該鏈表刪除,并加入到empty鏈表中,然后把mspan返回給當前P中運行的協程,解鎖。
? 歸還:先加鎖, 把mspan從empty鏈表刪除,然后加入到nonempty鏈表,解鎖。
mcache
? mspan作為內存管理的基本單位,顯然需要上一級單位來管理它:mcache。在runtime/mcache.go里面找到了這個結構體,只顯示關鍵字段。這是一個指針數組,再想到mspan結構的類型,可以想到是多條鏈表。
type mcache struct {
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
}
? 查看這個numSpanClasses,發現值是67<<1,等于134。意味著是上述class分類總數的兩倍,這是為何?原因是:上述的每種claas類型的span都有兩組列表,其中第一組列表中的對象包含了指針,第二組列表中表示的對象不包含指針。這樣做的目的是以空間換時間去提高GC掃描的性能,畢竟不用掃描不帶指針的那一條列表。mcache和span的對應關系如下圖所示:
? mcache在初始化的時候是空的,隨著程序的執行,會動態的從central中獲取并緩存下來。查看源碼我們發現,mcache結構體是沒有鎖的,是如何保證多線程安全的?每一個P(goroutine調度的GPM模型參考)都會有自己一個私有的mache,而每次只會有一個協程運行在同一個P上,也就是說每個P都擁有一個本地、私有化的mcache(內存池),所以不用加鎖。
小結
? 以64位4核處理器的Linux系統為例,4個邏輯處理器的go運行環境配置為例,虛擬內存堆區、mheap、mcentral、mcache和邏輯處理器p及goroutine的關聯關系如下入所示:
? 1、mheap創建了4M個heapArena結構體,把48位地址線管理的256T地址空間切分成一個個64MB的叫做Arena的塊。同時創建了包含134個元素的mcentral數組,每一個mcentral管理著同屬一類classid的span塊組成的鏈表。而每一類classid的span塊列表又分為帶指針的span塊和不帶指針的span塊,所以67類classid需要134個mcentral來管理。
? 2、每個mcentral中有兩個span鏈表:帶空余對象的可分配span鏈表和不帶空余對象或在mcache中已被使用的不可分配span列表。當P向本地的mcache申請span,而得不到時,mcache會向mcentral申請。mcentral為所有P共有,所以需要加鎖。
? 3、每一個邏輯P都有獨立的mcache用于緩存該邏輯處理器已申請的span,當有G運行在P上,且要去申請內存時,會優先從與該P對應綁定的mcache中申請,因為在P上同時只會有一個G在運行,且mcache專屬于P,所以不需要加鎖。與mcentral類似,每個mcache針對每個span類型的class維護兩條鏈表:帶指針的span塊和不帶指針的span塊,所以每個mcache中也有134條span塊的鏈表。
? 4、根據所管理對象大小,mspan一共被劃分為66類。mspan將分配得到的arena頁再度按所屬種類的對象大小再度切分,以 class類型24為例,占據一頁空間,對象大小為480bytes,因此該span被分為17個大小為480字節的小塊,一共使用8160字節,并有32字節被浪費掉。
### 內存分配
? 小于16字節的微小對象:
? 使用mcache的微小分配器,分配小于16B的對象。
? 16B~32KB的小對象:
? 由運行G所在P的去對應的mcache中查找對應大小的class,如果mcache分配失敗,則去mcentral中查找,否則再去mheap中申請新的頁用于mspan,并掛在mcentral與mcache中。
? 大于32K大對象:
? 由mheap直接申請,并分配在保存在mcentral的class0類型中。