栈分配和堆分配确实都发生在“对象分配器”的分配阶段, 但它们走的是不同的路径: 栈分配走的是编译期静态分配; 堆分配走的是运行时对象分配器(runtime.mallocgc)。
1.程序启动阶段
Go 运行时启动时(runtime 初始化),会:
向操作系统申请一大块虚拟内存(称为 arena);
由 页分配器(page allocator) 管理这块内存;
构建堆内存管理结构(mheap、mcentral、mcache)。
这部分只是“预留”内存,真正的对象分配还没发生。
2.用户程序触发分配(对象分配阶段)
当用户代码中创建变量时,比如:
x := MyStruct{}
编译器会在编译阶段决定这个对象是: 分配在栈上(stack allocation) 还是分配在堆上(heap allocation) 这个决策是通过 逃逸分析(Escape Analysis) 完成的。
3.栈分配的过程
如果编译器认为对象只在当前函数作用域内使用,不会被外部引用:
这个对象会直接分配在栈上;
不会调用运行时的内存分配器;
栈内存是随函数调用帧自动增长/释放的;
GC 不需要扫描或回收它。
> 关键:栈分配是编译期确定的,性能最好。
4.堆分配的过程
如果对象被闭包引用、返回地址或传递给其他 goroutine,则会发生逃逸:
编译器在生成代码时,会调用运行时的分配器 runtime.mallocgc;
mallocgc 会从当前 P 的 mcache 尝试获取一个合适的 span;
若 mcache 缓存不足,就从 mcentral → mheap 逐层申请;
分配完成后,GC 会在堆上追踪这个对象。
>关键:堆分配是运行时动态完成的,涉及 GC 管理。
5.回收阶段
当对象不再被引用时,GC 会标记并清除;
被清除的内存重新回收到 mcache / mcentral / mheap;
长期未使用的页可能由scavenger(拾荒器)归还给 OS。
6.对比栈和堆分配
| 类型 | 分配阶段 | 分配位置 | 分配速度 | 是否由 GC 管理 | 是否逃逸 |
|---|---|---|---|---|---|
| 栈分配 | 编译期(静态) | 每个 goroutine 的调用栈 | 极快 | 否 | 否 |
| 堆分配 | 运行时(动态) | 运行时堆(mheap) | 慢 | 是 | 是 |