分段栈虽然能够按需为当前 goroutine 分配内存并且及时减少内存的占用,但是它也存在一个比较大的问题:
如果当前 goroutine 的栈几乎充满,那么任意的函数调用都会触发栈的扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split)。
为了解决这个问题,Go 在 1.2 版本的时候不得不将栈的初始化内存从 4KB 增大到了 8KB。后来把采用连续栈结构后,又把初始栈大小减小到了 2KB。
# 连续栈
连续栈可以解决分段栈中存在的两个问题,其核心原理就是每当程序的栈空间不足时,初始化一片比旧栈大两倍的新栈并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤:
- 调用用 runtime.newstack 在内存空间中分配更大的栈内存空间;
- 使用 runtime.copystack 将旧栈中的所有内容复制到新的栈中;
- 将指向旧栈对应变量的指针重新指向新栈;
- 调用 runtime.stackfree销毁并回收旧栈的内存空间;
copystack会把旧栈里的所有内容拷贝到新栈里然后调整所有指向旧栈的变量的指针指向到新栈, 我们可以用下面这个程序验证下,栈扩容后同一个变量的内存地址会发生变化。
- package main
-
- func main() {
- var x [10]int
- println(&x)
- a(x)
- println(&x)
- }
-
- //go:noinline
- func a(x [10]int) {
- println(`func a`)
- var y [100]int
- b(y)
- }
-
- //go:noinline
- func b(x [100]int) {
- println(`func b`)
- var y [1000]int
- c(y)
- }
-
- //go:noinline
- func c(x [1000]int) {
- println(`func c`)
- }
程序的输出可以看到在栈扩容前后,变量x的内存地址的变化:
- 0xc000030738
- ...
- ...
- 0xc000081f38
是不是很简单呢?
本文转载自微信公众号「Go编程时光」,可以通过以下二维码关注。转载本文请联系Go编程时光公众号。