之前写过一篇关于Go内存分配的文章 - Go内存分配,讲的是Go堆内存的申请过程。对象除了可以在堆上分配,还有一个我们很熟悉的地方可以分配,栈。

toCF4P.jpg

栈内存分配

每一个协程都有自己的栈,协程访问栈的对象时,不需要加锁等同步操作

下面是一个例子:
调用main函数,Go在栈上分配内存

toPwLQ.jpg
调用square函数,Go在栈上分配内存

toPNz8.jpg
square执行完返回main时,上一步调用square时,分配的内存不会被回收。仅仅是移动了栈指针的位置。

toPdsg.jpg
调用println函数,Go移动栈指针的位置来分配内存。

toFUbQ.jpg

可以看出,在栈上分配内存的开销是很小的,只需要上下移动栈指针的位置来申请和释放内存。所以Go也倾向于在栈上分配内存。(Go prefers allocation on the stack

PS: 相比栈,在堆上分配内存的过程比较复杂,而且会使用垃圾回收来释放内存,垃圾回收时,会对程序造成延迟和降低吞吐量。

同时,Go使用的协程栈初始大小是2k,但可以动态的扩容和缩容。在开销上比线程栈小得多。

tok5wQ.jpg

逃逸分析

我们已经知道,在栈上分配内存远比在堆上分配内存好,但不是所有的对象都可以在栈上分配。

下面是一个例子
调用main函数,Go在栈上分配内存

toE3CR.jpg

调用answer函数,Go在栈上分配内存

toEl59.jpg

answer执行完返回main,此时Go移动了栈指针,上一步为answer分配的内存限制是无效的。
toEQUJ.jpg

调用println函数,Go移动栈指针来分配内存,此时覆盖了调用answer时分配的内存,指针指向的内存里的值已经被修改。
toEME4.jpg

Go编译器使用了逃逸分析来将对象分配到堆上,来解决上面的问题。

同样的程序

调用main函数,Go在栈上分配内存

toVO6P.jpg

调用answer函数,Go编译器知道在栈上为对象x分配内存是不安全的,此时会在堆上为对象x分配内存。

toVvm8.jpg

answer执行完返回main,此时指针n指向的是堆上的对象x

toZTBT.jpg

只有编译器知道对象应该被分配到哪里

我们无法决定对象被分配到栈还是堆,只有编译器知道。当编译器无法证明函数内的局部在函数执行完后,不会被外部引用,编译器会将变量分配到堆上,避免悬挂指针

我们可以使用go build -gcflags="-m 1"来了解对象如何分配。

tonUNF.jpg

一个我们平时写程序时注意的地方

toMj8e.jpg

左边的程序,每次调用read,都会在上分配对象。

这其实也是为什么io.Reader接口为什么将Read定义为Read(p []byte)(n int, err error),而不是Read(n int) (b []byte, err error)的原因。

toQMV0.jpg

参考资料

Understanding Allocations: the Stack and the Heap - GopherCon SG 2019

An Insight into Go Garbage Collection.pdf

栈空间管理

Last modification:June 10th, 2020 at 10:59 am
如果觉得我的文章对你有用,请尽情赞赏 🐶