栈分配和堆分配确实都发生在“对象分配器”的分配阶段, 但它们走的是不同的路径: 栈分配走的是编译期静态分配; 堆分配走的是运行时对象分配器(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)