詳解Go中記憶體分配
阿新 • • 發佈:2021-01-30
> 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com
>
> 本文使用的go的原始碼15.7
## 介紹
Go 語言的記憶體分配器就借鑑了 TCMalloc 的設計實現高速的記憶體分配,它的核心理念是使用多級快取將物件根據大小分類,並按照類別實施不同的分配策略。TCMalloc 相關的資訊可以看這裡:http://goog-perftools.sourceforge.net/doc/tcmalloc.html。
即如果要分配的物件是個小物件(<= 32k),在每個執行緒中都會有一個無鎖的小物件快取,可以直接高效的無鎖的方式進行分配;
如下:物件被分到不同的記憶體大小組中的連結串列中。
![Group 37](https://img.luozhiyun.com/20210129154004.png)
如果是個大物件(>32k),那麼頁堆進行分配。如下:
![Large Object Allocation](https://img.luozhiyun.com/20210129154013.png)
雖然go記憶體分配器最初是基於tcmalloc的,但是現在已經有了很大的不同。所以上面的一些結構會有些許變化,下面再慢慢絮叨。
因為記憶體分配的原始碼比較複雜,為了方便大家除錯,所以在進行原始碼分析之前,先看看是如何斷點彙編來進行除錯的。
### 斷點除錯彙編
目前Go語言支援GDB、LLDB和Delve幾種偵錯程式。只有Delve是專門為Go語言設計開發的除錯工具。而且Delve本身也是採用Go語言開發,對Windows平臺也提供了一樣的支援。本節我們基於Delve簡單解釋如何除錯Go彙編程式。專案地址:https://github.com/go-delve/delve
安裝:
```
go get github.com/go-delve/delve/cmd/dlv
```
首先編寫一個test.go的一個例子:
```go
package main
import "fmt"
type A struct {
test string
}
func main() {
a := new(A)
fmt.Println(a)
}
```
然後命令列進入包所在目錄,然後輸入`dlv debug`命令進入除錯:
```powershell
PS C:\document\code\test_go\src> dlv debug
Type 'help' for list of commands.
```
然後可以使用break命令在main包的main方法上設定一個斷點:
```powershell
(dlv) break main.main
Breakpoint 1 set at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8
```
通過breakpoints檢視已經設定的所有斷點:
```powershell
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x4377e0 for runtime.fatalthrow() c:/software/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x437860 for runtime.fatalpanic() c:/software/go/src/runtime/panic.go:1189 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8 (0)
```
通過continue命令讓程式執行到下一個斷點處:
```powershell
(dlv) continue
> main.main() c:/document/code/test_go/src/test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4bd30a)
3: import "fmt"
4:
5: type A struct {
6: test string
7: }
=> 8: func main() {
9: a := new(A)
10: fmt.Println(a)
11: }
12:
13:
```
通過disassemble反彙編命令檢視main函式對應的彙編程式碼:
```powershell
(dlv) disassemble
TEXT main.main(SB) C:/document/code/test_go/src/test.go
test.go:8 0x4bd2f0 65488b0c2528000000 mov rcx, qword ptr gs:[0x28]
test.go:8 0x4bd2f9 488b8900000000 mov rcx, qword ptr [rcx]
test.go:8 0x4bd300 483b6110 cmp rsp, qword ptr [rcx+0x10]
test.go:8 0x4bd304 0f8697000000 jbe 0x4bd3a1
=> test.go:8 0x4bd30a* 4883ec78 sub rsp, 0x78
test.go:8 0x4bd30e 48896c2470 mov qword ptr [rsp+0x70], rbp
test.go:8 0x4bd313 488d6c2470 lea rbp, ptr [rsp+0x70]
test.go:9 0x4bd318 488d0581860100 lea rax, ptr [__image_base__+874912]
test.go:9 0x4bd31f 48890424 mov qword ptr [rsp], rax
test.go:9 0x4bd323 e8e800f5ff call $runtime.newobject
test.go:9 0x4bd328 488b442408 mov rax, qword ptr [rsp+0x8]
test.go:9 0x4bd32d 4889442430 mov qword ptr [rsp+0x30], rax
test.go:10 0x4bd332 4889442440 mov qword ptr [rsp+0x40], rax
test.go:10 0x4bd337 0f57c0 xorps xmm0, xmm0
test.go:10 0x4bd33a 0f11442448 movups xmmword ptr [rsp+0x48], xmm0
test.go:10 0x4bd33f 488d442448 lea rax, ptr [rsp+0x48]
test.go:10 0x4bd344 4889442438 mov qword ptr [rsp+0x38], rax
test.go:10 0x4bd349 8400 test byte ptr [rax], al
test.go:10 0x4bd34b 488b4c2440 mov rcx, qword ptr [rsp+0x40]
test.go:10 0x4bd350 488d15099f0000 lea rdx, ptr [__image_base__+815712]
test.go:10 0x4bd357 4889542448 mov qword ptr [rsp+0x48], rdx
test.go:10 0x4bd35c 48894c2450 mov qword ptr [rsp+0x50], rcx
test.go:10 0x4bd361 8400 test byte ptr [rax], al
test.go:10 0x4bd363 eb00 jmp 0x4bd365
test.go:10 0x4bd365 4889442458 mov qword ptr [rsp+0x58], rax
test.go:10 0x4bd36a 48c744246001000000 mov qword ptr [rsp+0x60], 0x1
test.go:10 0x4bd373 48c744246801000000 mov qword ptr [rsp+0x68], 0x1
test.go:10 0x4bd37c 48890424 mov qword ptr [rsp], rax
test.go:10 0x4bd380 48c744240801000000 mov qword ptr [rsp+0x8], 0x1
test.go:10 0x4bd389 48c744241001000000 mov qword ptr [rsp+0x10], 0x1
test.go:10 0x4bd392 e869a0ffff call $fmt.Println
test.go:11 0x4bd397 488b6c2470 mov rbp, qword ptr [rsp+0x70]
test.go:11 0x4bd39c 4883c478 add rsp, 0x78
test.go:11 0x4bd3a0 c3 ret
test.go:8 0x4bd3a1 e82a50faff call $runtime.morestack_noctxt
.:0 0x4bd3a6 e945ffffff jmp $main.main
```
現在我們可以使用break斷點到runtime.newobject函式的呼叫上:
```powershell
(dlv) break runtime.newobject
Breakpoint 2 set at 0x40d426 for runtime.newobject() c:/software/go/src/runtime/malloc.go:1164
```
輸入continue跳到斷點的位置:
```powershell
(dlv) continue
> runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 (hits goroutine(1):1 total:1) (PC: 0x40d426)
Warning: debugging optimized function
1159: }
1160:
1161: // implementation of new builtin
1162: // compiler (both frontend and SSA backend) knows the signature
1163: // of this function
=>1164: func newobject(typ *_type) unsafe.Pointer {
1165: return mallocgc(typ.size, typ, true)
1166: }
1167:
1168: //go:linkname reflect_unsafe_New reflect.unsafe_New
1169: func reflect_unsafe_New(typ *_type) unsafe.Pointer {
```
print命令來檢視typ的資料:
```powershell
(dlv) print typ
*runtime._type {size: 16, ptrdata: 8, hash: 875453117, tflag: tflagUncommon|tflagExtraStar|tflagNamed (7), align: 8, fieldAlign: 8, kind: 25, equal: runtime.strequal, gcdata: *1, str: 5418, ptrToThis: 37472}
```
可以看到這裡列印的size是16bytes,因為我們A結構體裡面就一個string型別的field。
進入到mallocgc方法後,通過args和locals命令檢視函式的引數和區域性變數:
```powershell
(dlv) args
size = (unreadable could not find loclist entry at 0x8b40 for address 0x40ca73)
typ = (*runtime._type)(0x4d59a0)
needzero = true
~r3 = (unreadable empty OP stack)
(dlv) locals
(no locals)
```
### 各個物件入口
我們根據彙編可以判斷,所有的函式入口都是`runtime.mallocgc`,但是下面兩個物件需要注意一下:
### int64物件
`runtime.convT64`
```go
func convT64(val uint64) (x unsafe.Pointer) {
if val < uint64(len(staticuint64s)) {
x = unsafe.Pointer(&staticuint64s[val])
} else {
x = mallocgc(8, uint64Type, false)
*(*uint64)(x) = val
}
return
}
```
這段程式碼表示如果一個int64型別的值小於256,直接十三姨的是快取值,那麼這個值不會進行記憶體分配。
### string物件
`runtime.convTstring`
```go
func convTstring(val string) (x unsafe.Pointer) {
if val == "" {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(unsafe.Sizeof(val), stringType, true)
*(*string)(x) = val
}
return
}
```
由這段程式碼顯示,如果是建立一個為”“的string物件,那麼會直接返回一個固定的地址值,不會進行記憶體分配。
## 分析
### 分配器的元件
記憶體分配是由記憶體分配器完成,分配器由3種元件構成:`runtime.mspan`、`runtime.mcache`、`runtime.mcentral`、`runtime.mheap`。
**runtime.mspan**
```go
type mspan struct {
// 上一個節點
next *mspan
// 下一個節點
prev *mspan
// span集合
list *mSpanList
// span開始的地址值
startAddr uintptr
// span管理的頁數
npages uintptr
// Object n starts at address n*elemsize + (start << pageShift).
// 空閒節點的索引
freeindex uintptr
// span中存放的物件數量
nelems uintptr
// 用於快速查詢記憶體中未被使用的記憶體
allocCache uint64
// 用於計算mspan管理了多少記憶體
elemsize uintptr
// span的結束地址值
limit uintptr
...
}
```
`runtime.mspan`是記憶體管理器裡面的最小粒度單元,所有的物件都是被管理在mspan下面。
mspan是一個連結串列,有上下指標;
npages代表mspan管理的堆頁的數量;
freeindex是空閒物件的索引;
nelems代表這個mspan中可以存放多少物件,等於`(npages * pageSize)/elemsize`;
allocCache用於快速的查詢未被使用的記憶體地址;
elemsize表示一個物件會佔用多個個bytes,等於`class_to_size[sizeclass]`,需要注意的是sizeclass每次獲取的時候會sizeclass方法,將`sizeclass>>1`;
limit表示span結束的地址值,等於`startAddr+ npages*pageSize`;
例項圖如下:
![mcache](https://img.luozhiyun.com/20210129154022.png)
圖中alloc是一個擁有137個元素的mspan陣列,mspan陣列管理數個page大小的記憶體,每個page是8k,page的數量由spanclass規格決定。
**runtime.mcache**
```go
type mcache struct {
...
// 申請小物件的起始地址
tiny uintptr
// 從起始地址tiny開始的偏移量
tinyoffset uintptr
// tiny物件分配的數量
local_tinyallocs uintptr // number of tiny allocs not counted in other stats
// mspan物件集合,numSpanClasses=134
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
...
}
```
`runtime.mcache`是綁在併發模型GPM的P上,在分配微物件和小物件的時候會先去`runtime.mcache`中獲取,每一個處理器都會被分配一個執行緒快取`runtime.mcache`,因此從`runtime.mcache`進行分配時無需加鎖。
在`runtime.mcache`中有一個alloc陣列,是`runtime.mspan`的集合,`runtime.mspan`是 Go 語言記憶體管理的基本單元。對於[16B,32KB]的物件會使用這部分span進行記憶體分配,所以所有在這區間大小的物件都會從alloc這個數組裡尋找,下面會分析到。
**runtime.mcentral**
```go
type mcentral struct {
lock mutex
//spanClass Id
spanclass spanClass
// 空閒的span列表
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
// 已經被使用的span列表
empty mSpanList // list of spans with no free objects (or cached in an mcache)
//分配mspan的累積計數
nmalloc uint64
}
type mSpanList struct {
//連結串列頭
first *mspan // first span in list, or nil if none
//連結串列尾部
last *mspan // last span in list, or nil if none
}
```
當`runtime.mcache`中空間不足的時候,會去`runtime.mcentral`中申請對應規格的mspan。由於由於`runtime.mcentral`是公共資源,會有多個`runtime.mcache`向它申請`runtime.mspan`,因此必須加鎖。
在`runtime.mcentral`中,有spanclass標識,spanclass表示這個mcentral的型別,下面我們會看到,在分配[16B,32KB]大小物件的時候,會將物件的大小分成67組:
```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}
```
所以`runtime.mcentral`只負責一種spanclass規格型別,該規格的所有未被使用的空閒mspan會掛載到nonempty 連結串列上,已經被mcache拿走,未歸還的會掛載到empty 連結串列上,歸還後會再掛載到nonempty上。mspan會以連結串列的形式連結在`runtime.mcentral`上面。
![mcentral](https://img.luozhiyun.com/20210129154033.png)
**runtime.mheap**
```go
type mheap struct {
lock mutex
pages pageAlloc // page allocation data structure
//arenas陣列集合,一個二維陣列
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
//各個規格的mcentral集合
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
...
}
```
對於`runtime.mheap`需要關注central和arenas。central是各個規格的mcentral集合,在初始化的時候會通過遍歷class_to_size來進行建立;arenas是一個二維陣列,用來管理記憶體空間。arenas由多個`runtime.heapArena`組成,每個單元都會管理 64MB 的記憶體空間:
```go
const (
pageSize = 8192 // 8KB
heapArenaBytes = 67108864 // 64MB
pagesPerArena = heapArenaBytes / pageSize // 8192
)
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
zeroedBase uintptr
}
```
需要注意的是,上面的heapArenaBytes代表的64M只是在除windows以外的64 位機器才會顯示,在windows機器上顯示的是4MB。具體的可以看下面的官方註釋:
```go
// Platform Addr bits Arena size L1 entries L2 entries
// -------------- --------- ---------- ---------- -----------
// */64-bit 48 64MB 1 4M (32MB)
// windows/64-bit 48 4MB 64 1M (8MB)
// */32-bit 32 4MB 1 1024 (4KB)
// */mips(le) 31 4MB 1 512 (2KB)
```
L1 entries、L2 entries分別代表的是`runtime.mheap`中arenas一維、二維的值。
![mheap](https://img.luozhiyun.com/20210129154043.png)
### 給物件分配記憶體
我們通過對原始碼的反編譯可以知道,堆上所有的物件都會通過呼叫`runtime.newobject`函式分配記憶體,該函式會呼叫`runtime.mallocgc`:
```go
//建立一個新的物件
func newobject(typ *_type) unsafe.Pointer {
//size表示該物件的大小
return mallocgc(typ.size, typ, true)
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
dataSize := size
// 獲取mcache,用於處理微物件和小物件的分配
c := gomcache()
var x unsafe.Pointer
// 表示物件是否包含指標,true表示物件裡沒有指標
noscan := typ == nil || typ.ptrdata == 0
// maxSmallSize=32768 32k
if size <= maxSmallSize {
// maxTinySize= 16 bytes
if noscan && size < maxTinySize {
...
} else {
...
}
// 大於 32 Kb 的記憶體分配,通過 mheap 分配
} else {
...
}
...
return x
}
```
通過mallocgc的程式碼可以知道,mallocgc在分配記憶體的時候,會按照物件的大小分為3檔來進行分配:
1. 小於16bytes的小物件;
2. 在16bytes與32k之間的微物件;
3. 大於 32 Kb的大物件;
### 大物件分配
```go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
var s *mspan
shouldhelpgc = true
systemstack(func() {
s = largeAlloc(size, needzero, noscan)
})
s.freeindex = 1
s.allocCount = 1
x = unsafe.Pointer(s.base())
size = s.elemsize
...
return x
}
```
從上面我們可以看到分配大於32KB的空間時,直接使用largeAlloc來分配一個mspan。
```go
func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
// _PageSize=8k,也就是表明物件太大,溢位
if size+_PageSize < size {
throw("out of memory")
}
// _PageShift==13,計算需要分配的頁數
npages := size >> _PageShift
// 如果不是整數,多出來一些,需要加1
if size&_PageMask != 0 {
npages++
}
...
// 從堆上分配
s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero)
if s == nil {
throw("out of memory")
}
...
return s
}
```
在分配記憶體的時候是按頁來進行分配的,每個頁的大小是_PageSize(8K),然後需要根據傳入的size來判斷需要分多少頁,最後呼叫alloc從堆上分配。
```go
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {
var s *mspan
systemstack(func() {
if h.sweepdone == 0 {
// 回收一部分記憶體
h.reclaim(npages)
}
// 進行記憶體分配
s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)
})
...
return s
}
```
繼續看allocSpan的實現:
```go
const pageCachePages = 8 * unsafe.Sizeof(pageCache{}.cache)
func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) {
// Function-global state.
gp := getg()
base, scav := uintptr(0), uintptr(0)
pp := gp.m.p.ptr()
// 申請的記憶體比較小,嘗試從pcache申請記憶體
if pp != nil && npages < pageCachePages/4 {
c := &pp.pcache
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}
base, scav = c.alloc(npages)
if base != 0 {
s = h.tryAllocMSpan()
if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) {
goto HaveSpan
}
}
}
lock(&h.lock)
// 記憶體比較大或者執行緒的頁快取中記憶體不足,從mheap的pages上獲取記憶體
if base == 0 {
base, scav = h.pages.alloc(npages)
// 記憶體也不夠,那麼進行擴容
if base == 0 {
if !h.grow(npages) {
unlock(&h.lock)
return nil
}
// 重新申請記憶體
base, scav = h.pages.alloc(npages)
// 記憶體不足,丟擲異常
if base == 0 {
throw("grew heap, but no adequate free space found")
}
}
}
if s == nil {
// 分配一個mspan物件
s = h.allocMSpanLocked()
}
unlock(&h.lock)
HaveSpan:
// 設定引數初始化
s.init(base, npages)
...
// 建立mheap與mspan之間的聯絡
h.setSpans(s.base(), npages, s)
...
return s
}
```
這裡會根據需要分配的記憶體大小再判斷一次:
* 如果要分配的頁數小於`pageCachePages/4=64/4=16`頁,那麼就嘗試從pcache申請記憶體;
* 如果申請的記憶體比較大或者執行緒的頁快取中記憶體不足,會通過`runtime.pageAlloc.alloc`從頁堆分配記憶體;
* 如果頁堆上記憶體不足,那麼就mheap的grow方法從系統上申請記憶體,然後再呼叫pageAlloc的alloc分配記憶體;
下面來看看grow的向作業系統申請記憶體:
```go
func (h *mheap) grow(npage uintptr) bool {
// We must grow the heap in whole palloc chunks.
ask := alignUp(npage, pallocChunkPages) * pageSize
totalGrowth := uintptr(0)
nBase := alignUp(h.curArena.base+ask, physPageSize)
// 記憶體不夠則呼叫sysAlloc申請記憶體
if nBase > h.curArena.end {
av, asize := h.sysAlloc(ask)
if av == nil {
print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n")
return false
}
// 重新設定curArena的值
if uintptr(av) == h.curArena.end {
h.curArena.end = uintptr(av) + asize
} else {
if size := h.curArena.end - h.curArena.base; size != 0 {
h.pages.grow(h.curArena.base, size)
totalGrowth += size
}
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
...
return true
}
```
grow會通過curArena的end值來判斷是不是需要從系統申請記憶體;如果end小於nBase那麼會呼叫`runtime.mheap.sysAlloc`方法從作業系統中申請更多的記憶體;
```go
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
n = alignUp(n, heapArenaBytes)
// 在預先保留的記憶體中申請一塊可以使用的空間
v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
if v != nil {
size = n
goto mapped
}
// 根據頁堆的arenaHints在目標地址上嘗試擴容
for h.arenaHints != nil {
hint := h.arenaHints
p := hint.addr
if hint.down {
p -= n
}
if p+n < p {
// We can't use this, so don't ask.
v = nil
} else if arenaIndex(p+n-1) >= 1<>= uint(theBit + 1)
s.freeindex = freeidx
s.allocCount++
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
```
allocCache在初始化的時候會初始化成`^uint64(0)`,換算成二進位制,如果為0則表示被佔用,通過allocCache可以快速的定位待分配的空間:
![allocCache](https://img.luozhiyun.com/20210129154059.png)
```go
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
shouldhelpgc = false
// 當前span中找到合適的index索引
freeIndex := s.nextFreeIndex()
// 當前span已經滿了
if freeIndex == s.nelems {
if uintptr(s.allocCount) != s.nelems {
println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount != s.nelems && freeIndex == s.nelems")
}
// 從 mcentral 中獲取可用的span,並替換掉當前 mcache裡面的span
c.refill(spc)
shouldhelpgc = true
s = c.alloc[spc]
// 再次到新的span裡面查詢合適的index
freeIndex = s.nextFreeIndex()
}
if freeIndex >= s.nelems {
throw("freeIndex is not valid")
}
// 計算出來記憶體地址,並更新span的屬性
v = gclinkptr(freeIndex*s.elemsize + s.base())
s.allocCount++
if uintptr(s.allocCount) > s.nelems {
println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount > s.nelems")
}
return
}
```
nextFree中會判斷當前span是不是已經滿了,如果滿了就呼叫refill方法從 mcentral 中獲取可用的span,並替換掉當前 mcache裡面的span。
```go
func (c *mcache) refill(spc spanClass) {
s := c.alloc[spc]
...
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}
...
c.alloc[spc] = s
}
```
Refill 根據指定的sizeclass獲取對應的span,並作為 mcache的新的sizeclass對應的span。
```go
func (c *mcentral) cacheSpan() *mspan {
...
sg := mheap_.sweepgen
spanBudget := 100
var s *mspan
// 從清理過的、包含空閒空間的spanSet結構中查詢可以使用的記憶體管理單元
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}
for ; spanBudget >= 0; spanBudget-- {
// 從未被清理過的、有空閒物件的spanSet查詢可用的span
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
// 找到要回收的span,觸發sweep進行清理
s.sweep(true)
goto havespan
}
}
for ; spanBudget >= 0; spanBudget-- {
// 獲取未被清理的、不包含空閒空間的spanSet查詢可用的span
s = c.fullUnswept(sg).pop()
if s == nil {
break
}
if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
s.sweep(true)
freeIndex := s.nextFreeIndex()
if freeIndex != s.nelems {
s.freeindex = freeIndex
goto havespan
}
c.fullSwept(sg).push(s)
}
}
// 從堆中申請新的記憶體管理單元
s = c.grow()
if s == nil {
return nil
}
havespan:
n := int(s.nelems) - int(s.allocCount)
if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {
throw("span has no free objects")
}
//更新 nmalloc
atomic.Xadd64(&c.nmalloc, int64(n))
usedBytes := uintptr(s.allocCount) * s.elemsize
atomic.Xadd64(&memstats.heap_live, int64(spanBytes)-int64(usedBytes))
if trace.enabled {
// heap_live changed.
traceHeapAlloc()
}
if gcBlackenEnabled != 0 {
// heap_live changed.
gcController.revise()
}
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// 更新allocCache
s.refillAllocCache(whichByte)
// s.allocCache.
s.allocCache >>= s.freeindex % 64
return s
}
```
cacheSpan主要是從mcentral的spanset中去尋找可用的span,如果沒找到那麼呼叫grow方法從堆中申請新的記憶體管理單元。
獲取到後更新nmalloc、allocCache等欄位。
`runtime.mcentral.grow`觸發擴容操作從堆中申請新的記憶體:
```go
func (c *mcentral) grow() *mspan {
// 獲取待分配的頁數
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
// 獲取新的span
s := mheap_.alloc(npages, c.spanclass, true)
if s == nil {
return nil
}
// Use division by multiplication and shifts to quickly compute:
// n := (npages << _PageShift) / size
n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
// 初始化limit
s.limit = s.base() + size*n
heapBitsForAddr(s.base()).initSpan(s)
return s
}
```
grow裡面會呼叫`runtime.mheap.alloc`方法獲取span,這個方法在上面已經講過了,不記得的同學可以翻一下文章上面。
到這裡小物件的分配就講解完畢了。
### 微物件分配
```go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
dataSize := size
// 獲取mcache,用於處理微物件和小物件的分配
c := gomcache()
var x unsafe.Pointer
// 表示物件是否包含指標,true表示物件裡沒有指標
noscan := typ == nil || typ.ptrdata == 0
// maxSmallSize=32768 32k
if size <= maxSmallSize {
// maxTinySize= 16 bytes
if noscan && size < maxTinySize {
off := c.tinyoffset
// 指標記憶體對齊
if size&7 == 0 {
off = alignUp(off, 8)
} else if size&3 == 0 {
off = alignUp(off, 4)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
// 判斷指標大小相加是否超過16
if off+size <= maxTinySize && c.tiny != 0 {
// 獲取tiny空閒記憶體的起始位置
x = unsafe.Pointer(c.tiny + off)
// 重設偏移量
c.tinyoffset = off + size
// 統計數量
c.local_tinyallocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 重新分配一個記憶體塊
span := c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, _, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
//將申請的記憶體塊全置為 0
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 如果申請的記憶體塊用不完,則將剩下的給 tiny,用 tinyoffset 記錄分配了多少。
if size < c.tinyoffset || c.tiny == 0 {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
}
...
}
...
return x
}
```
在分配物件記憶體的時候做了一個判斷, 如果該物件的大小小於16bytes,並且是不包含指標的,那麼就可以看作是微物件。
在分配微物件的時候,會先判斷一下tiny指向的記憶體塊夠不夠用,如果tiny剩餘的空間超過了size大小,那麼就直接在tiny上分配記憶體返回;
![mchache2](https://img.luozhiyun.com/20210129154107.png)
這裡我再次使用我上面的圖來加以解釋。首先會去mcache數組裡面找到對應的span,tinySpanClass對應的span的屬性如下:
```
startAddr: 824635752448,
npages: 1,
manualFreeList: 0,
freeindex: 128,
nelems: 512,
elemsize: 16,
limit: 824635760640,
allocCount: 128,
spanclass: tinySpanClass (5),
...
```
tinySpanClass對應的mspan裡面只有一個page,裡面的元素可以裝512(nelems)個;page裡面每個物件的大小是16bytes(elemsize),目前已分配了128個物件(allocCount),當然我上面的page畫不了這麼多,象徵性的畫了一下。
上面的圖中還畫了在page裡面其中的一個object已經被使用了12bytes,還剩下4bytes沒有被使用,所以會更新tinyoffset與tiny的值。
## 總結
本文先是介紹瞭如何對go的彙編進行除錯,然後分了三個層次來講解go中的記憶體分配是如何進行的。對於小於32k的物件來說,go通過無鎖的方式可以直接從mcache獲取到了對應的記憶體,如果mcache記憶體不夠的話,先是會到mcentral中獲取記憶體,最後才到mheap中申請記憶體。對於大物件(>32k)來說可以直接mheap中申請,但是對於大物件來說也是有一定優化,當大物件需要分配的頁小於16頁的時候會直接從pageCache中分配,否則才會從堆頁中獲取。
## Reference
https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html
https://deepu.tech/memory-management-in-golang/
https://medium.com/@ankur_anand/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed
http://goog-perftools.sourceforge.net/doc/tcmalloc.html
https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-al