先来理解一些操作系统中如何管理内存的基本知识。
Stack vs. heap
应用程序内存可以简单分成堆与栈。
在Golang中,stack内存的分配由编译器自动分配与释放。stack区域中通常存储的是函数的参数,局部变量和调用函数的栈帧,这些参数在函数创建时分配,在函数退出时销毁。Goroutin对应于stack,一个堆栈通常包含了多个栈帧,这些栈帧描述了函数之间的调用关系,每个栈帧对应一个未返回的函数调用,它是以堆栈后进先出的形式来存储数据。

与堆栈不同的是,在运行时系统只有一个heap堆。准确的说,内存管理仅仅适用于堆中内存,程序可以在运行过程中主动从堆heap中申请内存,这些内存由Golang的内存分配器进行分配,并由垃圾收集器进行回收。
堆栈stack对于每个Goroutine都是唯一的,这意味着每个Goroutine运行中不需要锁定堆栈stack上的内存操作。但是,对于heap堆上的内存操作需要锁定来防止多线程之间的冲突。
局部变量与全局变量?
静态语言(比如C++)中,可能通过malloc/new向堆heap申请分配一块内存,使用结束后再销毁这块内存。如果不留心,很容易出现内存泄漏。
Golang语言中,你几乎不需要担心内存泄漏。对程序员而言,Golang的设计已经‘消除’了heap堆 与stack栈之间的区别。Golang编译器会在背后为我们做这些事情。你不需要区分:局部变量与全局变量。 在堆上还是在栈上分配一个变量,是由编译器动态分析的结果。
C/C++代码中如何处理栈和堆中变量?
在编写C/C++代码时,为了提高效率,我们经常从函数中返回一个指针,而不是返回一个复杂对象,以避免复杂对象拷贝构造函数执行的开销。
但这里面存在一个陷阱:在被调用函数内部定义一个局部变量,然后返回局部变量的指针。这些局部变量在栈stack中分配,一旦被调用函数执行结束,局部变量所在的栈帧就会被释放与销毁,而调用函数继续通过指针引用被调用函数栈帧空间的局部变量,会导致程序直接崩溃。
为了克服这个陷阱,一个 workaround方法是:向heap堆中申请一个变量,然后返回变量的地址。因为变量是在堆中创建的,所以在函数退出时并不会被销毁。
但是这个方法问题是:需要使用者牢记这个堆中变量,并恰当时候进行内存释放,避免内存的泄漏。
Golang中引入Escape Analysis,以确定变量究竟是在栈stack上还是堆heap上分配。
为什么需要逃逸分析?
Golang中的垃圾收集使得堆与栈对程序员完全透明,帮助他们完全专注于业务,高效完成业务编码,将这些复杂的内存管理机制交给编译器。
逃逸分析将变量合理分布到它应该在的地方(stack或者heap)。Golang分析函数退出后,变量没有被外部引用,就会将变量放入stack中。毕竟堆heap中内存根本要比栈上高效得多。反之,将变量放入heap进行管理。
如果将变量交给heap堆进行管理,则堆上不会像栈那样自动清理,这将导致GC经常回收,进而也会占用更多的系统开销。
逃逸分析如何工作?
简单地说,编译器将分析代码与它的生命周期,只有编译器能够证明在函数返回后,此变量不再被引用,才会将变量分配给栈。在其他情况下,变量会被分配到堆中。
Golang中并没有关键字来允许编译器直接标示将变量分配给heap或者stack,相反,编译器是通过分析代码来确定变量的分配位置。
简单总结:
- 如果在函数之外没有引用此变量,那么变量空间将由stack栈提供
- 如果在函数之外存在对此变量的引用,那么变量空间交由heap堆提供