Go 如何分配堆内存?
前言
前情回顾:
我们在使用make,new函数时,都会涉及到堆内存的分配。那Go是如何分配堆内存的呢?
堆结构
简单回顾下堆结构:
- heapArena是一个64MB的堆内存,里边有各种类别的mspan
- 要查找某类别的mspan时,为了效率,使用central作索引中心,通过它来映射到对应heapArena,找到mspan
- 但central是全局变量,有并发问题。于是每个处理器P各自拥有个mcache本地缓存。先从本地缓存获取mspan,没有再从索引中心查找并获取堆上的mspan
对象分级
Go将不同Object的大小分为三级:
- Tiny 微对象,大小(0,16B),不能为指针
- Small 小对象,大小 [16B,32KB]
- Large 大对象,大小(32KB,+∞)
区别处理:
- Tiny和Small对象分配到普通的mspan中,普通mspan的类别class在[1,67]范围内
- Large对象分配到量身定制的mspan中,特殊msan的类别class为0
分配方法
Tiny 微对象
- 从mcache中获取class为2的mspan, 该mspan每个单元大小为16B
- 多个微对象合并为一个16B存入mspan单元中
Small 小对象
- 先从mcache中获取mspan
- 没有再通过mcentral从heapArena中获取mspan
Large 大对象
- 直接从堆上的heapArena中分配空间
源码
src\runtime\malloc.go
的mallocgc
方法即堆内存分配主函数,以下为简化版mallocgc,只展示重要部分。
主要大纲:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 0 获取处理器P上的mcache缓存
mp := acquirem()
c := getMCache(mp)
// 1 分配size
if size <= maxSmallSize { // 要分配的size是否为Tiny或Small对象
if noscan && size < maxTinySize { // size为微对象
// 1.1 计算偏移量,合并到仍有空间的单元格中
off := c.tinyoffset
if size&7 == 0 {
off = alignUp(off, 8)
} else if goarch.PtrSize == 4 && size == 12 {
off = alignUp(off, 8)
} else if size&3 == 0 {
off = alignUp(off, 4)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 1.2 从mcache缓存中获取mspan
span = c.alloc[tinySpanClass]
v := nextFreeFast(span) // 获取一个空闲单元内存
if v == 0 {
// 1.3 mcache中没有多余的mspan,从central索引中心查找并获取mspan
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
size = maxTinySize
} else { // size为小对象
// 2.1 计算出小对象的类别
var sizeclass uint8
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
// 2.2 从mcache中获取mspan
span = c.alloc[spc]
v := nextFreeFast(span) // 获取一个空闲单元内存
if v == 0 {
// 2.3 mcache中没有多余的mspan,从central索引中心查找并获取mspan
v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size) // 清除内存之前存留的数据
}
}
} else { // size为大对象
shouldhelpgc = true
// 3.1 获取大对象的mspan
span = c.allocLarge(size, noscan)
span.freeindex = 1
span.allocCount = 1
size = span.elemsize
x = unsafe.Pointer(span.base())
if needzero && span.needzero != 0 {
if noscan {
delayedZeroing = true
} else {
memclrNoHeapPointers(x, size)
}
}
}
...
return x
}
重要函数nextFree
是如何通过mcentral获取mspan的,其路径为:
nextFree -> refill ->
使用mheap_.central[spc].mcentral.uncacheSpan(s)
,将mcache中爆满的mspan放入mcentral的队列中 ->
然后使用s = mheap_.central[spc].mcentral.cacheSpan()
,将空闲mspan出队。从而实现mcache与mcentral各自的mspan交换。->
最终返回获取的mspan
扩展
如果mcache和central中都没有mspan怎么办?
那mheap就要扩容,向操作系统申请一个heapArena了。
在调用nextFree -> refill -> cacheSpan中,如果仍没有mspan的话,就调用
grow ->mheap_.alloc,得到新的heapArena,就有空闲的mspan可用了。
总结
- Go将对象按大小分为三种
- 微小对象先用mcache
- mcache的mspan填满后,与mcentral交换新的空闲mspan
- mcentral不足后,在新的heapArena中开辟新的mspan
- 大对象直接在heapArena中开辟新的mspan
转载自:https://juejin.cn/post/7231896581477597243