Go语言之内存堆栈分配(逃逸分析)

Stack and Heep Memory in golang

Posted by alovn on June 29, 2021

Go程序在运行时刻每个协程维护一个栈,这个栈是预申请的内存段,它被作为一个内存池供开辟内存使用。每个协程的初始栈比较小,协程在运行时候将按照需要进行增长或收缩一个协程开辟在栈上的内存只能在此协程内部被引用,其它协程是无法访问的。

  • 在栈上开辟内存块要比堆上快的多。
  • 在栈上开辟的内存块不需要被垃圾回收处理。

当一个协程的栈大小改变时,一个新的内存段将申请给此栈使用。原先内存块可能被转移到新的内存段上,也就是说这些内存块的地址将会改变。

每个协程维护栈有一个最大限制,64位系统上默认值是1GB,32位系统上默认是250MB。可以在运行时刻通过runtime/debug.SetMaxStack来修改这个值。

如果一个内存块没有被开辟在任何一个栈上,我们就说它开辟在了堆上。开辟在堆上的内存块可以被多个协程并发的访问。

如果编译器觉察到一个内存块在运行时将会被多个协程访问,或者不能断定此内存块是否只会被一个协程访问,则此内存块将会被开辟在堆上。

在运行时刻,如果一个局部变量的值被开辟在堆上,那么一般说这个局部变量逃逸到了堆上。每一个逃逸到堆上的值若仍在被使用,那么它肯定被至少一个栈上的值所引用着。如果一个T类型的局部变量逃逸到了堆上,则在运行时,一个*T类型的隐式指针被创建在栈上,这个指针存储着T类型局部变量在堆上的地址,这样就形成了一个从栈到堆的引用关系,另外,编译器还会将所有对该局部变量的引用替换为对此指针的引用。若这个变量不再被使用就会进入垃圾回收处理流程。可以使用go build -gcflags -m命令查看代码中哪些局部变量在运行时刻逃逸到了堆上。

Go语言中还存在如下的堆逃逸情况:

  • 如果一个结构体值的某个字段逃逸到了堆上,那么整个结构体的值也逃逸到了堆上。
  • 如果数组中某个元素逃逸到了堆上,那么整个数组也逃逸到了堆上。
  • 如果一个切片的某个元素逃逸到了堆上,那么此切片中所有元素都将逃逸到堆上。
  • 如果一个变量值被一个逃逸到堆上的变量引用,那么这个变量也将逃逸到堆上。