前一篇講了Go的調(diào)度機(jī)制和相關(guān)源碼恨狈,這里說一下內(nèi)存的管理策严,代碼片段也都是基于Go 1.12。
簡要的背景
一個(gè)程序要運(yùn)行起來放典,操作系統(tǒng)會(huì)分配一塊很大的虛擬內(nèi)存(或者說虛擬空間)供使用烦感,程序?qū)嶋H可能只使用很小的物理內(nèi)存巡社。可以通過ps
去查看vss(虛擬)和rss(實(shí)際)的部分手趣。
虛擬內(nèi)存空間一般會(huì)分為代碼段晌该,數(shù)據(jù)段,堆绿渣,棧等:
程序執(zhí)行時(shí)朝群,函數(shù)中定義的各種變量,一般位于堆和棧中(上圖中黃色怯晕、綠色潜圃、白色)部分。為了能動(dòng)態(tài)分配內(nèi)存舟茶,讓程序運(yùn)行過程中靈活使用,操作系統(tǒng)提供了很多相關(guān)函數(shù),例如mmap/munmap吧凉,brk/sbrk隧出,madvise,set_thread_area/get_thread_area等阀捅。C語言中malloc胀瞪,free就是對這些基礎(chǔ)函數(shù)的封裝。
而其他高層語言中饲鄙,例如Go凄诞,把這些對內(nèi)存的操作完全屏蔽起來,寫程序的過程中根本感受不到忍级。
Go語言本身的實(shí)現(xiàn)中帆谍,內(nèi)存由runtime自主管理。runtime與操作系統(tǒng)的交互并沒有使用malloc這樣的函數(shù)轴咱,而是通過匯編(或cgo)直接調(diào)用了mmap等函數(shù)汛蝙。其內(nèi)存分配核心思想非常類似于TCMalloc,不過由于部分特性(gc等)的需求朴肺,在TCMalloc的算法和設(shè)計(jì)上也做了部分修改窖剑。
基本概念
Go程序在啟動(dòng)的時(shí)候會(huì)向操作系統(tǒng)申請一塊內(nèi)存,然后分塊管理戈稿。核心的結(jié)構(gòu)是mheap, mcentral, mcache, mspan西土。
- mheap
全局,負(fù)責(zé)從os申請鞍盗、釋放內(nèi)存需了。 - mcentral
全局,將內(nèi)存按mspan劃分橡疼,統(tǒng)一管理援所。訪問需加鎖。mcentral劃分為_NumSizeClasses(目前是67)種mspan欣除,每種又分為非空(nonempty住拭,代表可以分配)和空(empty,可能是已滿历帚,可能是已分配給mcache在使用滔岳,代表不能分配)。非空和空分別形成雙向鏈表挽牢,方便訪問谱煤。 - mcache
每個(gè)P持有自己的mcache,于是獲取內(nèi)存的時(shí)候禽拔,無鎖訪問mcache刘离。mcache也劃分為_NumSizeClasses(目前是67)種mspan室叉。
注:很多文章中,認(rèn)為mcache中的每種mspan也是鏈表硫惕,但是我從代碼中看來茧痕,好像mcache對每種mspan只會(huì)保存一個(gè)。這一點(diǎn)待更進(jìn)一步理解恼除。 - mspan
設(shè)計(jì)的精華踪旷,類似tcmalloc的機(jī)制。將一個(gè)或多個(gè)內(nèi)存頁形成一種mspan豁辉,一種mspan只負(fù)責(zé)分配固定size的內(nèi)存(不足的時(shí)候令野,會(huì)向上補(bǔ)足,例如申請7byte徽级,會(huì)使用8 byte類型的mspan)气破。mspan的列表見sizeclasses.go,里面詳細(xì)記錄了每一種mspan的分配規(guī)則灰追,可存儲(chǔ)object多少堵幽,浪費(fèi)率等。
這種設(shè)計(jì)可以保證分配內(nèi)存盡可能快弹澎,且能減少碎片的產(chǎn)生朴下。
相關(guān)代碼
- mallocinit
最初申請內(nèi)存的地方在runtime/mallocinit(malloc.go)中。
func mallocinit() {
if class_to_size[_TinySizeClass] != _TinySize {
throw("bad TinySizeClass")
}
testdefersizes()
if heapArenaBitmapBytes&(heapArenaBitmapBytes-1) != 0 {
// heapBits expects modular arithmetic on bitmap
// addresses to work.
throw("heapArenaBitmapBytes not a power of 2")
}
// Copy class sizes out for statistics table.
for i := range class_to_size {
memstats.by_size[i].size = uint32(class_to_size[i])
}
初始化的過程相較于之前的版本苦蒿,已經(jīng)有了非常大的變化殴胧,不過核心也依然是作各種檢查斩郎,然后初始化mheap(主要是初始化各種allocator氏涩,方便以后allocator真正分配內(nèi)存。mcentral就是這個(gè)時(shí)候初始化的)枫虏,嘗試為當(dāng)前的M(M是什么报强,參見調(diào)度的文章)分配mcache以及根據(jù)操作系統(tǒng)設(shè)置正確的arenaHints灸姊。
- heapArena
之前的Go版本里,arena是大小512G的(網(wǎng)上很多圖介紹秉溉,自行Google)力惯。但我在go1.12源碼里發(fā)現(xiàn)不是這樣了。mheap_.arenas
是一個(gè)二維數(shù)組[L1][L2]heapArena
(heapArena存的是對應(yīng)arena的元數(shù)據(jù))召嘶,維度以及arena本身的大小和尋址bit位數(shù)相關(guān)父晶,每個(gè)arena的起始地址按對應(yīng)大小對齊。heapArena
這些元數(shù)據(jù)本身不存在heap里面弄跌。以我自己的機(jī)器(64位)為例甲喝,屬于下圖中的第一行。32位機(jī)器上是4M铛只。計(jì)算方法是:
1 << 48 = 2 ^ 48 = 64M * 1 * 4M (即只有1行埠胖,4M列糠溜,每個(gè)大小64M)
mheap_.arena
的這個(gè)二維數(shù)組中有些有對應(yīng)的堆,有些沒有(那么就是nil押袍,例如垃圾回收之后诵冒,把內(nèi)存還給了操作系統(tǒng))凯肋。go的內(nèi)存分配器總是嘗試分配連續(xù)的arena谊惭,這樣某些大的span可以跨越arena。
heapArena中bitmap用每2個(gè)bit記錄一個(gè)指針大形甓(8byte)的內(nèi)存信息圈盔,主要用于gc。spans是一個(gè)數(shù)組悄雅,長度等于一個(gè)heap中的頁數(shù)(每頁大小為8k驱敲,頁數(shù)可能為64M/8k,不同架構(gòu)會(huì)不同)宽闲,每個(gè)頁可能會(huì)指向一個(gè)span(即一個(gè)mspan指針)众眨。實(shí)際上,對于分配的容诬,空閑的和未分配的span娩梨,指針情況可能各不相同。pageInUse和pageMarks都是標(biāo)記頁的览徒,按位處理狈定。
- mache初始化和fixalloc
再扯回來,第一次分配mcache习蓬,這里就涉及到真正的內(nèi)存分配了纽什。allocmcache
中可以看到,之前已經(jīng)初始化了cachealloc躲叼,這里調(diào)用alloc函數(shù)芦缰,走到的是fixalloc
的alloc函數(shù):
func (f *fixalloc) alloc() unsafe.Pointer {
if f.size == 0 {
print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n")
throw("runtime: internal error")
}
if f.list != nil {
v := unsafe.Pointer(f.list)
f.list = f.list.next
f.inuse += f.size
if f.zero {
memclrNoHeapPointers(v, f.size)
}
return v
}
if uintptr(f.nchunk) < f.size {
f.chunk = uintptr(persistentalloc(_FixAllocChunk, 0, f.stat))
f.nchunk = _FixAllocChunk
}
v := unsafe.Pointer(f.chunk)
if f.first != nil {
f.first(f.arg, v)
}
f.chunk = f.chunk + f.size
f.nchunk -= uint32(f.size)
f.inuse += f.size
return v
}
nchunk代表目前剩余的大小,size是目標(biāo)大小枫慷。如果nchunk小于size让蕾,就從系統(tǒng)申請一塊(_FixAllocChunk
這么大)內(nèi)存,然后按照size這個(gè)固定的大小一點(diǎn)一點(diǎn)用流礁。對于用完釋放的涕俗,又會(huì)存在list屬性中,供之后再次使用(使用的時(shí)候清零)神帅。實(shí)際分配內(nèi)存是通過persistentalloc一步步調(diào)用sysAlloc進(jìn)而調(diào)用mmap分配的再姑。
allocmcache
接下來就會(huì)把mcache中各個(gè)spanclass對應(yīng)的mspan初始化為空mspan。
需要注意的是找御,上面這個(gè)特殊的M是這樣初始化mcache元镀。其實(shí)mcache是應(yīng)該跟著P的绍填,所以其他的mcache的初始化都是在procresize這個(gè)函數(shù)里,它在schedinit()中栖疑,位于mallocinit()之后讨永。
- newobject和mallocgc
上面講了啟動(dòng)過程的各種初始化。初始化完畢遇革,程序執(zhí)行的時(shí)候卿闹,在堆上的對象是通過runtime.newobject函數(shù)來分配的。
什么時(shí)候分配在堆萝快,什么時(shí)候分配在棧锻霎,這又是另外一個(gè)值得長篇探討的問題,稱為“逃逸分析”揪漩,這里暫不深入旋恼。
newobject的代碼位于malloc.go中,它直接調(diào)用了mallocgc:
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
而mallocgc里面就是包含了分配內(nèi)存時(shí)最核心的順序和步驟奄容,即小對象從mcache的freelist中開始分配冰更,大對象(大于32k,即maxSmallSize)直接從堆上分配昂勒。小對象的分配又分為是否是tiny對象(以maxTinySize=16為界)蜀细。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
size = maxTinySize
} else {
var sizeclass uint8
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
} else {
sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
span := c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(v), size)
}
}
} else {
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
}
var scanSize uintptr
if !noscan {
// If allocating a defer+arg block, now that we've picked a malloc size
// large enough to hold everything, cut the "asked for" size down to
// just the defer header, so that the GC bitmap will record the arg block
// as containing nothing at all (as if it were unused space at the end of
// a malloc block caused by size rounding).
// The defer arg areas are scanned as part of scanstack.
if typ == deferType {
dataSize = unsafe.Sizeof(_defer{})
}
heapBitsSetType(uintptr(x), size, dataSize, typ)
if dataSize > typ.size {
// Array allocation. If there are any
// pointers, GC has to scan to the last
// element.
if typ.ptrdata != 0 {
scanSize = dataSize - typ.size + typ.ptrdata
}
} else {
scanSize = typ.ptrdata
}
c.local_scan += scanSize
}
...
從核心分配代碼看,先根據(jù)待分配對象size算出實(shí)際使用的sizeClass(即mspan的不同分類)叁怪,然后算出spanClass审葬,這里spanClass(本身是一個(gè)uint8)包含了span的size信息和span是否需要scan(用于gc)的信息,相當(dāng)于mcache中alloc數(shù)組的index奕谭,數(shù)組是按一個(gè)noscan sizeClass一個(gè)scan sizeClass這樣交替排列下去的涣觉。
算好這些, 就調(diào)用nextFreeFast嘗試分配(mcache中)血柳,如果不成功官册,調(diào)用nextFree分配(還是mcache中),再不成功难捌,調(diào)用memclrNoHeapPointers分配(mcentral或mheap中)膝宁。
nextFreeFast相對簡單。mspan中allbits記錄著哪些元素是已分配的根吁,哪些未分配员淫。alloccache用數(shù)字按位代表freeindex開始的
func nextFreeFast(s *mspan) gclinkptr {
theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?
if theBit < 64 {
result := s.freeindex + uintptr(theBit)
if result < s.nelems {
freeidx := result + 1
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
s.allocCache >>= uint(theBit + 1)
s.freeindex = freeidx
s.allocCount++
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
但為了講清這里的方式,需要簡單了解mspan的內(nèi)部結(jié)構(gòu)击敌,貼出一張網(wǎng)上盜的圖介返,簡單明了:
所以可以看到,mspan里面用位圖記錄了元素分配與否,直接查找即可圣蝎。
nextFree稍微復(fù)雜刃宵,因?yàn)橐幚矸峙洳怀晒Γ^續(xù)向mcentral申請內(nèi)存的邏輯:
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex()
if freeIndex == s.nelems {
// The span is full.
if uintptr(s.allocCount) != s.nelems {
println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount != s.nelems && freeIndex == s.nelems")
}
c.refill(spc)
shouldhelpgc = true
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
if freeIndex >= s.nelems {
throw("freeIndex is not valid")
}
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
}
其中徘公,refill函數(shù)會(huì)從mcentral甚至mheap獲取mspan牲证。
這塊是最主要的分配邏輯。剩下的是tiny object(注意关面,需要不含指針且足夠刑古邸)和large object的分配。它們做特殊處理都是因?yàn)樘』蛱箸择桑贿m合適配到某些固定大小键闺。在許多場景中tiny object這種分配策略能顯著優(yōu)化性能。