Go 原子内存分配
一、Go 内存分配到底是什么?
Go 内存分配 = 线程缓存 + 中心缓存 + 堆管理 + 虚拟内存布局
目标:高并发、无锁 / 轻锁、少碎片、极快分配
三大核心组件
- mcache:P 私有缓存(无锁,最快)
- mcentral:全局中心缓存(每个规格一个,加锁)
- mheap:堆总控(管理所有内存)
分配规则
- 小对象(0~16B) :用 mcache 槽位(tiny) 分配
- 中小对象(16B~32KB) :用 mcache 规格化块(span) 分配
- 大对象(>32KB) :直接走 mheap 分配
二、Go 内存布局(虚拟地址空间)
Go 把虚拟内存分成 4 个核心区域:
- arena:真正的堆内存(最大)
- bitmap:标记指针 / GC 信息
- spans:记录每个地址属于哪个 span
- stacks:栈内存
三、核心结构体
文件:runtime/malloc.go runtime/sizeclasses.go
1. mcache(P 私有,无锁,分配最快)
// mcache 每个 P 一个,私有缓存,无锁
type mcache struct {
// tiny 分配:小于 16 字节的小对象
tiny uintptr // tiny 分配起始地址
tinyOffset uintptr // tiny 当前偏移量
tinyAllocs uintptr // tiny 分配计数
// 规格化内存块:共 67 种规格(8B,16B,24B...32KB)
alloc [numSizeClasses]*mspan // 每种规格一个span链表
}
2. mcentral(全局中心缓存,原子 / 锁操作)
// 每个 size class 一个 mcentral
type mcentral struct {
sizeclass int32 // 规格ID
nonempty mspanList // 有空闲块的 span
empty mspanList // 无空闲块的 span
mutex mutex // 保护并发安全
}
3. mspan(内存块最小管理单元)
// span:管理一组连续 pages(8KB)
type mspan struct {
start uintptr // 起始地址
npages uintptr // 占用页数
freeIndex uintptr // 下一个空闲块索引
freeCount uintptr // 空闲块数量
sizeclass int32 // 规格ID
state spanState // 状态:mSpanFree/mSpanInUse
}
4. mheap(全局堆控制器)
var mheap_ mheap
type mheap struct {
// 所有 mcentral,共67个
central [numSizeClasses]mcentral
// 空闲页管理
freeSpans [128]mspanList // 按页数分组的空闲span
// arena 区域
arena struct {
start uintptr
end uintptr
}
}
四、内存分配核心流程
1. 分配 tiny 对象(<16B)
- 直接用 mcache.tiny
- 多个 tiny 共享一个块
- 无锁、原子偏移量增加
- 最快!
2. 分配小对象(16B~32KB)
- 计算 size class(67 种规格)
- 从 mcache 取对应 span
- mcache 有空闲块 → 无锁分配
- mcache 没有 → 向 mcentral 申请
- mcentral 没有 → 向 mheap 申请
- mheap 向操作系统申请内存
3. 分配大对象(>32KB)
- 直接走 mheap
- 分配连续页
- 不进缓存
五、核心分配源码
1. mallocgc 入口
// mallocgc 分配内存
// size: 大小
// typ: 类型
// needZero: 是否清零
func mallocgc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
// 1. 超大对象直接走 heap
if size > maxSmallSize {
return largeAlloc(size, typ, needZero)
}
// 2. tiny 对象分配(<16B)
if size <= maxTinySize {
return tinyAlloc(size, typ, needZero)
}
// 3. 中小对象(16B~32KB)
return smallAlloc(size, typ, needZero)
}
2. tinyAlloc 微对象分配(原子无锁)
func tinyAlloc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
mp := acquirem() // 获取当前M
c := mp.p.ptr().mcache // 获取P的mcache
// 原子偏移量 + size
offset := c.tinyOffset
newOffset := offset + size
// 如果当前块足够
if newOffset <= maxTinySize {
c.tinyOffset = newOffset // 原子更新偏移
return unsafe.Pointer(c.tiny + offset)
}
// 不够,申请新的tiny块
return refillTinyCache(size, typ, needZero)
}
3. smallAlloc 小对象分配
func smallAlloc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
// 计算 size class
sc := getSizeClass(size)
c := getMCache()
span := c.alloc[sc]
// 当前 span 有空闲块
if span.freeCount > 0 {
// 无锁分配
ptr := span.start + span.freeIndex*size
span.freeIndex = *(*uintptr)(unsafe.Pointer(span.start + span.freeIndex))
span.freeCount--
return unsafe.Pointer(ptr)
}
// mcache 空了,从 mcentral refill
c.refill(sc)
return smallAlloc(size, typ, needZero)
}
4. mcache.refill 从 mcentral 获取 span
func (c *mcache) refill(sc int32) {
// 加锁,从 mcentral 获取一个有空闲的 span
span := mheap_.central[sc].mcentral.cacheSpan()
c.alloc[sc] = span
}
六、Go 内存分配 完整流程图
1. 整体分配主流程
2. Tiny 微对象分配(最快)
3. mcache → mcentral → mheap 缓存链
4. 大对象分配流程
七、10 个细节
1. mcache 与 P 绑定,无锁,最快
每个 P 自己用自己的缓存,完全无锁。
2. tiny 分配是极致优化
多个小对象共享一个块,原子偏移量,无锁。
3. 共 67 种 size class
覆盖 8B ~ 32KB,减少内存碎片。
4. mcentral 用锁,但粒度极小
只锁对应规格的 central,并发冲突极低。
5. span 是最小管理单元
默认 8KB 一页,一个 span 包含 N 页。
6. mheap 全局唯一
管理所有内存、页、span。
7. 向 OS 申请内存用 mmap
一次性大片申请,减少系统调用。
8. 分配时不触发 GC
只有内存达到阈值才触发 GC。
9. 所有本地操作都是原子 / 无锁
保证高并发性能。
10. Go 内存分配是目前业界最快的之一
无锁 + 分层缓存 + 预分配。
八、总结
- mcache:P 私有,无锁,最快
- mcentral:全局规格缓存
- mheap:总控
- tiny <16B:共享块,原子偏移
- 16B~32KB:size class 分级管理
- >32KB:直接走堆
- 全程无锁 / 轻锁,高并发神器