c/c++中动态分配的内存需要我们手动释放,导致我们平时在写程序时,如履薄冰。但是这样做也有好处,那就是程序员可以完全掌握内存。但是缺点也是很明显的,那就是我们经常忘记释放内存,导致内存泄漏。很多现代语言都加上了垃圾回收机制

Go的垃圾回收,让堆和栈对程序员保持透明,真正解放了程序员的双手,让他们可以专注于业务。

逃逸分析这种“骚操作”把变量合理地分配到它该去的,“找准自己的位置”,即使你是用new申请到的内存,如果我发现你竟然在退出函数以后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析之后发现在退出函数之后还有在其他地方在引用,那我就把你分配到堆上,真正地做到“按需分配”,提前实现共产主义。

如果变量都分配到堆上,堆不像栈可以自动清理,它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用cpu容量的25%)。

堆和栈相比,栈适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令,“PUSH”和“RELEASE”,分配和释放;而堆分配到内存首先需要去找到一块大小合适的内存块,之后通过垃圾回收才能释放。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也减少go的压力,提高程序的运行速度。

逃逸分析是怎么完成的

Go逃逸分析的最进本原则是,如果一个函数返回一个变量的引用,那么它就会发生逃逸。任何时候,编译器会分析代码的特征和代码生命周期,Go中变量只有在编译器可以证明在函数返回后不会再被引用,才分配到栈上,其他情况都是分配到堆上。

go语言里没有关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。一个变量取地址,可能会被分配到堆上,但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会分配到栈上。

简单来说,编译器会根据变量是否被外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放在栈中
  2. 如果函数外部存在引用,则必定放在堆中

说一个最常见的情形,slice由于append操作超出其容量,因此导致slice重新分配,这种情况下,由于编译时slice的初始化大小已知情况下,将会被分配在栈上。如果slice底层存储基于运行时进行扩展,则它将分配在堆上。

对于go程序员来说,编译器的逃逸分析规则不需要掌握,我们只需要通过go build -goflags -m命令来观察变量逃逸情况就行了。

参考

【Golang】Golang内存逃逸是什么?怎么避免内存逃逸?