Go语言以其高效的并发能力闻名,但其高性能的基础之一是极其高效的内存分配器。Go的内存分配器基于Google的TCMalloc(Thread-Caching Malloc)思想,采用了精妙的三级缓存结构,极大地减少了分配过程中的锁竞争压力。
本文将深入探讨Go内存分配的核心单元 mspan,并解释它如何在三级缓存(mcache、mcentral、堆)中协同工作,实现几乎无锁的小对象分配。
1. 为什么需要三级缓存?
传统的内存分配器通常使用全局锁来保护共享内存池,在高并发场景下,这会导致严重的性能瓶颈。Go的目标是让大多数小对象分配在不获取任何锁的情况下完成,从而支持数以万计的Goroutines并行运行。
Go通过将内存管理工作分解为三个层次来实现这一目标:
2. 第一级:mcache (Goroutine/P 局部缓存)
mcache 是分配速度最快、最核心的组件。每个操作系统线程(对应一个Go运行时处理器 P)都拥有一个私有的 mcache 实例。这个实例存储着预先分配好的、按照不同大小分类的内存块(即 mspan)。
关键特性: Goroutine 可以在其关联的 P 上分配小对象时,直接从 mcache 中获取内存,无需任何锁保护。
// 抽象概念:mcache 结构 (每个 P 私有)
type mcache struct {
// alloc 数组存储着不同大小类别(SizeClass)的 mspan 列表
alloc [NumSizeClasses]*mspan
// ... 其他本地统计信息
}
// 流程:分配一个小于 32KB 的对象时:
// 1. 确定对象所属的 SizeClass。
// 2. 从 mcache.alloc[SizeClass] 中取出 span,直接分配。
如果 mcache 中某个 SizeClass 的 mspan 耗尽了,它才会向上一级缓存请求新的内存。
3. 第二级:mcentral (中央缓存)
mcentral 是共享的资源池,用于在不同的 mcache 之间平衡和分发内存。系统中每种大小类别(SizeClass)都有一个对应的 mcentral 实例。
关键特性: mcentral 负责管理所有该 SizeClass 的 mspan 列表。当 mcache 缺页时,它会从 mcentral 索取一个带有空闲对象的 mspan。
虽然 mcentral 是共享的,因此需要加锁保护,但由于只有在 mcache 耗尽时才会访问它,所以锁竞争的频率极低。
// 抽象概念:mcentral 结构 (每种 SizeClass 共享一个)
type mcentral struct {
lock mutex
sizeclass int32
// 拥有空闲对象的 mspan 列表
nonempty mspanList
// 已被清空或等待 GC 的 mspan 列表
empty mspanList
}
4. mspan:内存管理的基本单元
mspan 是 Go 内存分配器的核心抽象。它代表了一块连续的、已被切分成固定大小对象的内存页。
一个 mspan 总是只服务于一个特定的 SizeClass。例如,一个 mspan 可能被切割成许多 64字节的对象,另一个 mspan 可能被切割成许多 1KB 的对象。
// mspan 结构简化版
type mspan struct {
next, prev *mspan // 用于连接 mspanList (如 mcentral 中的 nonempty/empty)
startAddr uintptr // span 起始地址
npages uintptr // span 占用的页数
// ... 还有许多字段用于追踪哪些对象已被分配
}
当 mcache 从 mcentral 拿走一个 mspan 时,它实际上是拿走了一个已经被结构化、准备好分配的内存块。一旦该 mspan 被添加到 mcache 中,mcache 对其的操作就是无锁的。
5. 第三级:堆 (Heap)
如果 mcentral 发现自己的 nonempty 列表中没有可用的 mspan,它会向 Go 运行时堆(mheap)请求一批新的操作系统页,然后将这些页切分、初始化为一个或多个新的 mspan,并将其添加到自己的 nonempty 列表中。
大对象处理: 任何大于 32KB 的对象,由于无法放入任何 SizeClass 的 mspan 中,会直接向 mheap 请求连续的页来分配。这些大对象分配需要获取全局锁,但它们占总分配量的比例很小。
总结
Go通过 mcache、mcentral 和 mspan 的巧妙配合,实现了高效的内存分配:
- 最小化锁竞争: 绝大多数(98%以上)的小对象分配都在本地 mcache 上完成,无需任何锁。
- 效率和均衡: mspan 保证了每次从中央缓存获取的都是一大批可以立即使用的、同类型的小对象,减少了与中央锁交互的次数。
- 大对象管理: 大对象直接从堆中分配,避开 mcache 和 mcentral 的小对象路径,保持了分配路径的简洁性。
汤不热吧