这里整理 Go 内存分配器中 mspan、mcache、mcentral、mheap 和 heapArena 的职责与使用场景,以及它们与 Thread Cache、Central Cache 和 Heap 三层结构的对应关系。
内存分配器的分层结构
Go 的内存分配器分三层,从上到下是:
- Thread Cache(线程缓存)
- Central Cache(中央缓存)
- Heap(堆)
关键组件及其关系
对应到运行时实现,三层分别落在 mcache、mcentral、mheap 上,加上 mspan 和 heapArena 两个基础结构,一共五个关键组件。
关系图示
Thread Cache (mcache)
|
Central Cache (mcentral)
|
Heap (mheap)
|
heapArena组件详解及使用场景
1. Thread Cache(线程缓存)/ mcache
mcache 是每个 P(Processor)私有的缓存,用于分配小对象(小于 32 KB)。它包含多个 mspan,每个 mspan 用于存储相同大小类别的对象。
使用场景:
- 适用于频繁分配和释放的小对象,减少线程之间的竞争,提高分配效率。
例子:
func allocateSmallObject(size int) {
// 分配一个小对象(< 32 KB)
mem := make([]byte, size)
// 使用内存防止被优化掉
mem[0] = 1
fmt.Println("Allocated small object")
}2. Central Cache(中央缓存)/ mcentral
mcentral 管理每个大小类别的 mspan,负责从 mheap 获取新的 mspan,并将用完的 mspan 归还给 mheap,是 mcache 和 mheap 之间的中间层。
使用场景:
- 当
mcache中的内存不足时,从mcentral获取新的mspan。 - 当
mcache中有多余内存时,将其归还给mcentral。
例子(运行时内部伪代码示意,不可直接编译运行):
func allocateFromCentralCache(sizeClass int) {
// 从中央缓存分配内存
span := mcentral.alloc(sizeClass)
// 使用 span 分配对象
obj := span.allocateObject()
fmt.Println("Allocated object from central cache")
}3. Heap(堆)/ mheap
mheap 是全局的堆内存管理器,负责从操作系统获取内存并管理 mspan 的分配和回收。它提供大块内存(页),并将这些内存分成 mspan 供 mcentral 使用。
使用场景:
- 适用于大对象分配(>= 32 KB)。
- 当
mcentral请求新的mspan时,从mheap获取内存。
例子:
func allocateLargeObject(size int) {
// 分配一个大对象(>= 32 KB)
mem := make([]byte, size)
// 使用内存防止被优化掉
mem[0] = 1
fmt.Println("Allocated large object")
}4. mspan
mspan 是一组连续的内存页,用于分配对象。mspan 按大小类别(class)划分,每个类别包含大小相似的对象。
使用场景:
- 管理相同大小的对象,以提高分配和释放效率。
- 由
mcache和mcentral使用,用于对象分配。
例子(运行时内部伪代码示意,不可直接编译运行):
func allocateFromSpan(span *mspan, size int) {
// 从 span 分配对象
obj := span.allocate(size)
fmt.Println("Allocated object from span")
}5. heapArena
heapArena 是 mheap 从操作系统获取的大块内存区域,每个 heapArena 包含 64 MB 的内存。用于管理和分配内存页。
使用场景:
mheap从操作系统获取内存时,将其分割成heapArena。- 提供基础内存给
mheap管理。
例子(运行时内部伪代码示意,不可直接编译运行):
func allocateFromHeapArena(size int) {
// 从 heapArena 分配内存
arena := newHeapArena(size)
fmt.Println("Allocated memory from heap arena")
}内存分配流程
小对象分配(<32 KB)
- 当 goroutine 请求分配一个小对象时,
mcache首先检查是否有足够的内存可用。 - 如果
mcache有足够的内存,直接分配并返回对象。 - 如果
mcache没有足够的内存,会向mcentral请求一个新的mspan。 - 如果
mcentral没有可用的mspan,会向mheap请求一个新的mspan。
- 当 goroutine 请求分配一个小对象时,
大对象分配(>=32 KB)
- 当 goroutine 请求分配一个大对象时,直接向
mheap请求内存。 mheap分配相应大小的内存页并返回。
- 当 goroutine 请求分配一个大对象时,直接向
总结
Go 的内存分配器按 mcache、mcentral、mheap 三层分工:小对象在 P 私有的 mcache 上就地分配,线程之间几乎不竞争;不够了才向 mcentral 要 mspan,再不够由 mheap 向操作系统要内存。分层可能带来内存碎片,换来的是贴合 Go 并发模型的分配速度。
下面是整合的代码示例,展示了各个组件的使用:
package main
import (
"fmt"
"sync"
)
func allocateSmallObject(size int) {
// 分配一个小对象(< 32 KB)
mem := make([]byte, size)
// 使用内存防止被优化掉
mem[0] = 1
fmt.Println("Allocated small object")
}
func allocateFromCentralCache(sizeClass int) {
// 模拟从中央缓存分配内存
fmt.Println("Allocated object from central cache of size class", sizeClass)
}
func allocateLargeObject(size int) {
// 分配一个大对象(>= 32 KB)
mem := make([]byte, size)
// 使用内存防止被优化掉
mem[0] = 1
fmt.Println("Allocated large object")
}
func allocateFromSpan(spanSize, objSize int) {
// 模拟从 span 分配对象
fmt.Printf("Allocated object of size %d from span of size %d\n", objSize, spanSize)
}
func allocateFromHeapArena(size int) {
// 模拟从 heapArena 分配内存
fmt.Println("Allocated memory from heap arena of size", size)
}
func main() {
var wg sync.WaitGroup
wg.Add(3)
// 小对象分配
go func() {
defer wg.Done()
allocateSmallObject(16 * 1024) // 16 KB
}()
// 从中央缓存分配对象
go func() {
defer wg.Done()
allocateFromCentralCache(1) // Size class 1
}()
// 大对象分配
go func() {
defer wg.Done()
allocateLargeObject(64 * 1024) // 64 KB
}()
wg.Wait()
fmt.Println("All allocations finished")
}这个例子串了一遍小对象、大对象和中央缓存三条分配路径;真正的分配逻辑都在 Go 运行时内部,应用代码看到的只是 make 这类调用。