Go语言设计与实现之栈内存管理
文章目录
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 以上的栈内存,提高本地分配栈内存的性能。
栈分配
- 如果栈空间较小(小于32KB),使用全局栈缓存或者线程缓存上固定大小的空闲链表分配内存
- 如果栈空间较大(大于32KB),从全局的大栈缓存
runtime.stackLarge
中获取内存空间 - 如果栈空间较大并且
runtime.stackLarge
空间不足,在堆上申请一片大小足够内存空间
栈扩容
主要指连续栈:
- 在内存空间中分配更大的栈内存空间;
- 将旧栈中的所有内容复制到新栈中;
- 将指向旧栈对应变量的指针重新指向新栈;
- 销毁并回收旧栈的内存空间;
栈缩容
运行时只会在栈内存使用不足 1/4 时进行缩容,缩容也会调用扩容时使用的 runtime.copystack
开辟新的栈空间。触发栈的缩容时,新栈的大小会是原始栈的一半,不过如果新栈的大小低于程序的最低限制 2KB,那么缩容的过程就会停止。
文章作者 cold-bin
上次更新 2023-10-30