go的栈内存

栈内存与堆内存不一样,一般GC扫描的对象属于堆区,局部变量、函数参数等都分配到栈内存,而全局变量等会分配到堆区。

那么栈内存并不由GC来释放没有使用的内存,而是由编译器自动分配和释放:随着函数生命周期创建和销毁

局部变量不一定分配在栈内存,也可能会逃逸到堆区。

1
2
3
4
func NewObj() *int {
    a:=1
    return &a
}

设计原理

线程栈

大多数OS创建线程的大小默认都在2MB到4MB左右,而且后续不会扩缩容,大小固定

这对于瞬时创建大量并发任务,但是所需栈空间较小的场景,固定栈不太合适。

go是在用户态自己实现了一个可以自扩缩容的栈内存。

逃逸分析

逃逸分析指的是确定哪些变量应该分配到栈上,哪些变量应该分配到堆上。逃逸分析两个不变性:

  • 指向栈对象的指针不能存在于堆中;
  • 指向栈对象的指针不能在栈对象回收后存活;

栈内存空间

  • 分段栈

    go1.3之前,采用分段栈的方式划分栈内存。协程初始化时,会分配固定大小的栈空间,之后随着函数调用越来越多,会创建新的栈空间与被调用函数的栈空间链表相连

    缺点

    • 如果当前 Goroutine 的栈几乎充满,那么任意的函数调用都会触发栈扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split);
    • 一旦 Goroutine 使用的内存越过了分段栈的扩缩容阈值,运行时会触发栈的扩容和缩容,带来额外的工作量;
  • 连续栈

    连续栈可以解决分段栈中存在的两个问题。连续栈扩容时,会创建新的一个大栈,再将以前旧栈的数据拷贝过来,销毁旧栈

栈操作

栈初始化

运行时使用全局的 runtime.stackpool 和线程缓存中的空闲链表分配 32KB 以下的栈内存,使用全局的 runtime.stackLarge 和堆内存分配 32KB 以上的栈内存,提高本地分配栈内存的性能。

栈分配

  1. 如果栈空间较小(小于32KB),使用全局栈缓存或者线程缓存上固定大小的空闲链表分配内存
  2. 如果栈空间较大(大于32KB),从全局的大栈缓存 runtime.stackLarge 中获取内存空间
  3. 如果栈空间较大并且 runtime.stackLarge 空间不足,在堆上申请一片大小足够内存空间

栈扩容

主要指连续栈:

  1. 在内存空间中分配更大的栈内存空间;
  2. 将旧栈中的所有内容复制到新栈中;
  3. 将指向旧栈对应变量的指针重新指向新栈
  4. 销毁并回收旧栈的内存空间;

栈缩容

运行时只会在栈内存使用不足 1/4 时进行缩容,缩容也会调用扩容时使用的 runtime.copystack 开辟新的栈空间。触发栈的缩容时,新栈的大小会是原始栈的一半,不过如果新栈的大小低于程序的最低限制 2KB,那么缩容的过程就会停止。