你好,我是小小酥,今天给大家分享的是go语言面试大概率会被问到的问题之一,go语言的内存分配,在今天的文章里,我们将首先从全局的角度去了解go语言内存管理的整体设计,接着会对内存管理的各个组件从功能到数据结构一一进行剖析,最后我们会通过分析源码的方式将各个组件串起来从而学习到go语言中一块内存到底是怎么分配的,对于核心的代码我都加上了注释。
鸟瞰全局
go语言的内存分配采用了TCMalloc的算法,并在此基础上进行了一定程度的优化,其核心思想如下
内存切割
众所周知,一大片内存如果按照顺序分配给不同的对象,当对象被回收时,这一大块内存会产生不同大小的内存碎片,降低了内存的利用率。
因此go语言中通过将一大片内存切割成多个不同规格(size class)的小块内存,称为mspan,然后创建对象时根据实际需要按需分配,很大程度上减少了内存碎片提高内存的利用率。
多级管理
go语言采用多级内存分配的方式进行内存管理:每个P都会维护一个本地的内存池,称为mcache,mcache内包含了所有规格的mspan,
当mcache本地没有足够内存空间时,mcache会从一套称为mcentral的内存组件里申请内存,每一种规格的mspan都有一个mcentral对象,每个mcentral中都包含了对应规格的mspan对象列表。
那么当mcentral中的mspan对象链表也没有空闲的内存空间了呢?这时mcental会从一个称为mheap的对象中申请内存,mheap内部维护的不再是mspan而是一页一页的内存了,当mcentral向mheap申请内存时,mheap会根据mcentral对应规格划分一连串页作为新的mspan分配给mcentral。
到这里我们已经知道了mcache维护了每种规格大小的一个mspan对象,mcentral维护的是对应规格mspan的对象列表,mheap维护的是一页一页的内存。进行内存分配时优先从本地mcache中分配,mcache维护在P上,一个P绑定的M统一时间只会执行一个goroutine,因此这里不会有锁竞争,当mcache内存不足时向对应规格的mcentral申请mspan,此时也只会在对应规格的mcentral上加锁,只有当mcentral的内存空间也不足时,才会向全局的mheap申请内存而加全局锁。
通过将内存进行了多级管理,内存分配时降低了锁的粒度,提升了内存分配的性能。
除了内存切割和多级管理,go语言在内存分配中还加入了针对微小对象的分配器(tiny allocator),为mspan加上gc标记等方式来做内存优化,这些在后面的内容中都会进行详细的讲解,就不在这里展开了,本节的最后根据上述的内容,我们再来看看由mcache,mcentral,mheap组成的结构图。
组件剖析
mcache
每个工作线程都会绑定一个mcache
mcache用span classes作为索引管理多个用于分配的mspan,它包含所有规格的mspan
mcache在初始化的时候是没有任何mspan资源的,在使用的过程中,会动态的从mcentral申请,之后会缓存下来,当对象小于等于32kb时,使用mcache对相应规格的mspan进行分配。
源码
type mcache struct {
nextSample uintptr // 分配多少大小的堆时会触发堆采样
scanAlloc uintptr // 分配的可扫描堆字节数
tiny uintptr // 堆指针,指向当前tiny块的起始指针,如果当前无tiny块,则为nil
tinyoffset uintptr // 当前tiny块的位置
alloc [numSpanClasses]*mspan // 当前p的分配规格信息
stackcache [_NumStackOrders]stackfreelsit
flushGen uint32
}
mspan
mspan是GO中内存管理的基本单元,是由一片连续8kb的页组成的大块内存。
mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表。
每个mspan按照它自身的属性size class的大小分割成若干个object,每个object可存储一个对象,并且会使用一个位图来标记其尚未使用的object。属性size class决定object大小,而mspan只会分配给和object尺寸大小接近的对象。
每个size class有两个mspan,也就是有两个span class,其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。
size class
mspan的size class有67种,每种mspan分割的object大小是8*2n的倍数
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}
根据mspan的size class可以得到它划分的object大小,比如size class等于3,object 大小就是32b,32b大小的object可以存储对象大小范围是17b-32b,在class_to_allocnpages数组里对应的页数是1
源码
type mspan struct {
next *mspan // 链表向后的指针
prev *mspan // 链表向前的指针
startAddr uintptr // 起始地址,也既管理页的地址,指向arena区域的某个位置,表示这个mspan的起始地址
npages uintptr // 管理的页数
nelems uintptr // 块个数,标识有多少个块可供分配
allocBits *gcBits // 分配的位图,每一位代表一个快是否已分配
allocCount uint16 // 已分配的块数
spanclass spanClass // class表中的class id,和size class相关
elemsize uintptr // class表中的对象大小,也既块大小
}
mcentral
为所有mcache提供切分好的mspan资源,每个central保存一个特定大小的全局mspan列表,包括已分配的和未分配的,每个mcentral对应一种mspan,mcentral被所有mcache共同享有,存在多个goroutine竞争的情况,因此会消耗锁资源。
每个mcentral是两个mspans列表,空闲对象c->notempty和完全分配对象c->empty
源码
type mcentral struct {
spanclass spanClass // 当前规格大小
partial [2]spanSet // 存在空闲对象的spans列表
full [2]spanSet // 无空闲对象的spans列表
}
type spanSet struct {
spineLock mutex
spine unsafe.Pointer
spineLen uintptr
spineCap uintptr
index headTailIndex
}
func (c *mcentral) init(spc spanClass) {
c.spanclass = spc
lockInit(&c.partial[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.partial[1].spineLock, lockRankSpanSetSpine)
lockInit(&c.full[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.full[1].spineLock, lockRankSpanSetSpine)
}
mheap
如果mcache和mcentral都没有适宜规则的大小内存,这时候就会向mheap申请一块内存,然后按指定规格划分为一些列表,并将其添加到相同规格大小的mecntral的not empty list后面。
go没法使用工作线程的本地缓存mcache和mecntral尚管理超过32kb的内存分配,所以对那些超过32kb的内存申请,会直接从堆上分配对应数量的内存页。
源码
type mheap struct {
lock mutex // 全局锁
pages pageAlloc // 页面分配的数据结构
sweepgen uint32 // 清扫完成
sweepDrained uint32 // 清扫完成标记
sweepers uint32 // 活动清扫调用sweepone数
allspans []*mspan // 所有申请过的mspan都会放在allspans
_ uint32 // align uint64 fields on 32-bit for atomics
pagesInUse uint64 // 统计mSpanInUse中的spans页数
pagesSwept uint64 // 本轮清扫的页数
pagesSweptBasis uint64 // 用作清扫率atomically
sweepHeapLiveBasis uint64
sweepPagesPerByte float64
scavengeGoal uint64
reclaimIndex uint64
reclaimCredit uintptr
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena // 堆arena 映射。它指向整个可用虚拟地址空间的每个 arena 帧的堆元数据;
heapArenaAlloc linearAlloc
arenaHints *arenaHint
arena linearAlloc
allArenas []arenaIdx // 是每个映射arena的arenaIndex 索引。可以用以遍历地址空间。
sweepArenas []arenaIdx // []arenaIdx 指在清扫周期开始时保留的 allArenas 快照
markArenas []arenaIdx // []arenaIdx 指在标记周期开始时保留的 allArenas 快照
curArena struct {
base, end uintptr
}
_ uint32
central [numSpanClasses]struct { // mcentral
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
spanalloc fixalloc
cachealloc fixalloc
specialfinalizeralloc fixalloc
specialprofilealloc fixalloc
specialReachableAlloc fixalloc
speciallock mutex
arenaHintAlloc fixalloc
unused *specialfinalizer
}
源码分析
newobject
newobject用于在队中分配内存,内部调用了mallocgc
runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
mallocgc
mallocgc是分配内存的关键函数,是分配内存的入口,它会根据需要分配内存大小的不同将请求分发到不同的方法上去,执行流程如下
-
如果需要分配的内存小于16b,则执行tiny allocator,微小对象分配器
- tiny allocator首先会从当前mcache的tiny指针指向的内存空间中获取内存,获取成功就直接返回
- 如果mcahce.tiny指针指向的内存空间中内存不足,则调用mcache.nextFreeFast方法获取tinySpanClass规格大小的mspan来重置tiny指针指向的内存空间
- 如果mcache.alloc[tinySpanClass]也没有足够的内存空间了,则需要调用mcache.nextFree方法申请从mcentral中获取对应规格大小的span
-
如果需要分配的内存大于16b且小于32kb,则执行small allocator,小对象分配器、
- ·small allocator先根据需要分配的内存大小计算出对应映射的span规格大小
- 接着从mcache.alloc[spanClass]中获取对应大小的object
- 如果mcache.alloc[spanClass]中的内存不足,则依然调用mcache.nextFree方法从mcentral中获取对应规格大小的span
-
如果需要分配的内存大于等于32kb,则执行big allocatrot,大对象分配器
-
go内存分配对于大于32kb的内存申请,会直接调用mcache.allocLarge方法,从mheap上直接分配内存
-
下面是对mallocgc源码注释,为了方便查看,其中省略了与内存分配没有直接关系的代码
runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarkterminiation")
}
if size == 0 {
return unsafe.Pinter(&zerbase)
}
... //gc相关判断
c := getMcache() // 获取当前p绑定的mcache
if c == nil {
throw("mallocgc called without a P or outside bootstrapping")
}
var span *mspan
var x unsafe.Pointer
noscan := typ == nil || typ.ptrdata == 0
isZeroed := true
// 小于32kb,执行小对象分配流程
if size <= maxSmallSize {
// 无指针 && 小于16b,执行微小对象分配流程
if noscan && size < maxTinySize {
// 对齐操作
off := c.tinyoffset
if size&7 == 0 {
off = alignUp(off, 8)
} else if sys.PtrSize == 4 && size == 12 {
off = alignUp(off, 2)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
// 判断对齐后的大小依然小于16b && mcache.tiny != 0
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off) // 根据对齐的地址获取一片内存
c.tinyoffset = off + size // 调整mcache.tinyoffset
c.tinyAllocs++ // 调整mcache.tinyAllocs
mp.mallocing = 0
releasem(mp) // 释放当前m
return x //返回对象
}
span = c.alloc[tinySpanClass]
v := nextFreeFast(span) // 如果当前mcache.tiny已经没有足够的内存了,则需要从mcache.alloc中拿一部分tinySpanClass对应的内存
if v == 0 {
v, span, shouldhepgc = c.nextFree(tinySpanClass) // 如果mcache.alloc[tinySpanClass]也没有,则要通过c.nextFree到mcentral中获取对应的mspan了
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x) // 调整mcache.tiny
c.tinyoffset = size // 调整mcache.tinyoffset
}
size = maxTinySize
} else {
var sizeclass uint8
if size <= smallSizeMax-8 { // 需要分配的size<1kb,则从size_to_class8获取到对应的sizeclass
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else { //需要分配的size>1kb,则从size_to_class128获取到对应的sizeclass
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass]) // 根据sizeclass映射到实际分配的大小
spc := makeSpanClass(sizeclass, noscan)
span = c.alloc[spc]
v := nextFreeFast(span) // 从mcache.alloc获取到对应规格的mspan
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc) // mcache.alloc未获取到则从mcentral中获取
}
x = unsafe.Pointer(v) // 分配内存
}
} else { // 需要的内存大于32kb
... // gc相关
span, isZeroed = c.allocLarge(size, needzero && !noscan, noscan) // 调用mcache.allocLarge方法从mheap获取内存
x = unsafe.Pointer(span.base())
}
... // gc相关
return x
}
nextfree
前文提到,当mcache.alloc中对于规格的span也没有空闲object的时候,则需要通过nextFree从mcentral中申请新的mspan,核心流程如下
- 通过nextFreeIndex()获取当前span下一个空闲object的索引
- 判断获取到的索引值等于该规格mspan.nelems证明当前mcache.alloc[spanClass]确实没有空闲的object了
- 调用mcache.refiil从mcentral中申请新的mspan
- 申请到mspan以后做一些长度的校验并再次调用nextFreeIndex()获取一个空闲的object,同时更新mspan.allocCount
下面是对nextfree源码注释
runtime/malloc.go
func (c *mcache) nextFree(spc spanClass) (v gclinkPtr, s *mspan, shouldhelpgc) {
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex() //s.nextFreeIndex()用于获取span下一个空闲object的索引
if freeIndex == s.nelems { // 如果获取到的索引等于当前span的规格长度(s.nelems),说明这个span已经没有空闲的object了
c.refill(spc) // 调用mcache.refill从mcentral申请新的mspan
shouldhelpgc = true // gc相关
s = c.alloc[spc] // 再次从mcache.alloc中获取mspan
freeIndex = s.nextFreeIndex() // 获取mspan中下一个空闲object的索引
}
if freeIndex >= s.nelems { // 再次进行长度的校验
throw("freeIndex is not valid")
}
v = gclinkptr(freeIndex*s.elemsize + s.base()) //根据object的索引获取对应指针的地址
s.allocCount++ //mspan分配出去的对象数+1
return
}
refill
当mcache.alloc[spanClass],会调用refill方法再通过mcentral.cacheSpan()从mcentral中申请新的mspan
下面是对refill源码注释,为了方便查看,其中省略了与内存分配没有直接关系的代码
runtime/mcache.go
func (c *mcache) refill(spc spanClass) {
s := c.alloc[spc] // 从mcache获取对应规格的mspan
if uintptr(s.allocCount) != s.nelems { // 如果对应mspan的allocCount != mspan的规格大小,证明该mspan依然还有空闲的object
throw("refill of span with free space remaining")
}
if s != &emptymspan { //emptymspan是一个不包含空闲object的虚拟mspan,如果和当前mspan不相等
mheap_.central[spc].mcentral.uncacheSpan(s) //对mceantral的span队列进行清理
}
s = mheap_.central[spc].mcentral.cacheSpan() // 从对应规格的mcentral中获取一个新的mspan
if s == nil {
throw("out of memory")
}
if uintptr(s.allocCount) == s.nelems {
throw("span has no free space")
}
... // gc相关
c.alloc[spc] = s // 将s放入c.alloc[spc]中了
}
cachespan
cacheSpan负责从对应规格的mcentral中获取mspan,执行流程如下:
-
尝试从清理过的、包含空闲对象的spanSet结构中获取mspan
- 对mspan做初始化的操作,并返回
-
尝试从未清理过、包含空闲对象的spanSet结构中获取mspan
- 获取到以后触发sweep进行清理
- 对mspan做初始化的操作,并返回
-
尝试从未清理过、不包含空闲对象的spanSet结构中获取mspan
- 获取到以后触发sweep进行清理
- 清理以后判断获取到的mspan是否还有空闲的object
- 对mspan做初始化的操作,并返回
-
以上方式都未获取到说明mcentral对应规格的mspan列表中也没有空闲内存了,这时通过调用mchentral.grow方法申请从mheap中分配内存
-
对mspan做初始化的操作,并返回
-
下面是对cacheSpan源码注释,为了方便查看,其中省略了与内存分配没有直接关系的代码
runtime/mcentral.go
func (c *mcentral) cacheSpan() {
var s *mspan // 定义一个mspan的指针
sl := newSweepLocker() // 因为mcentral是公共的,需要加锁
sg := sl.sweepGen
spanBudget := 100 // 限制从spanSet中获取mspan的次数
if s = c.partialSwept(sg).pop(); s != nil { // 尝试从清理过的、包含空闲对象的spanSet结构中获取mspan
goto havespan // 如果获取到了则直接执行havespan
}
for ; spanBudget >= 0; spanBudget-- { // 尝试100次
s = c.partialUnswept(sg).pop() // 尝试从未清理过的、包含空闲对象的spanSet结构中获取mspan
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true) // 触发sweep进行清理
sl.dispose()
goto havespan // 执行havespan
}
}
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop() // 尝试从未清理过的、不包含空闲对象的spanSet结构中获取mspan
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true) // 触发清理
freeIndex := s.nextFreeIndex() // 获取mspan下一个空闲object的索引
if freeIndex != s.nelems { // 如果索引值和mspan的规格不一致,说明有空闲对象
s.freeindex = freeIndex
sl.dispose()
goto havespan // 执行havespan
}
// Add it to the swept list, because sweeping didn't give us any free space.
c.fullSwept(sg).push(s.mspan)
}
// See comment for partial unswept spans.
}
s = c.grow() // 如果从mcentral中都没有获取到合适的mspan,那只能到mheap中去获取了
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")
}
// 对mspan进行一些初始化的操作
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return s
}
grow
当mcentral也没有相应spanClass的内存时,从mheap申请分配对应span
runtime/mcentral.go
func (c *mcentral) grow() *mspan {
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) // 获取待分配的页数
size := uintptr()
s, _ := mheap_.alloc(npages, c.spanclass, true) // 从堆栈中分配新的mspan
if s == nil {
return nil
}
// 初始化操作
n := s.divideByElemSize(npages << _PageShift)
s.limit = s.base() + size * n
heapBitsForAddr(s.base()).initSpan(s)
return s
}
参考
- zhuanlan.zhihu.com/p/27807169
- goog-perftools.sourceforge.net/doc/tcmallo…
- zhuanlan.zhihu.com/p/29216091
- www.sohu.com/a/322736774…
- studygolang.com/articles/29…
- segmentfault.com/a/119000003…
下期预告
到这里,相信你已经对go语言的内存分配有了一定程度上的理解了,面对面试官的提问你也一定能侃侃而谈了吧~,下一次我将继续为你分享go语言面试常问的go语言的垃圾回收,敬请期待!
如果你喜欢我的文章,欢迎关注我的公众号,万分感谢!