C/C++中,调用malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露。
但在Golang中,基本不会担心内存泄露。Golang中使用new函数得到的内存不一定就在堆上。一个变量是在堆上分配,还是在栈上分配,是经过编译器的逃逸分析之后得出的结论。
堆和栈的区别对程序员“模糊化”了,这一切都是Go编译器在背后帮我们完成的。

基础概念

在C/C++中,为了提高效率,常常将 pass-by-value(传值)“升级”成 pass-by-reference,以此避免构造函数的运行,并且直接返回一个指针。

但这里隐藏了一个很大的坑:在函数内部定义了一个局部变量,然后返回这个局部变量的指针。这些局部变量是在栈上分配的,一旦函数执行完毕,为该变量分配的内存会被销毁,任何对这个返回值作的操作,都可能会出现问题,甚至导致程序直接崩溃。

一个解决办法是在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在堆上创建的,所以函数退出时不会被销毁。但是这样也会有隐患,忘记使用delete销毁,这样就会发生内存泄露。

但是在Go中这种返回局部变量指针的做法却不会有任何问题,这归功于Go语言里编译器的 逃逸分析。它是编译器在执行静态代码分析后,对内存管理进行的优化和简化。

逃逸分析的原则

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

第一条之所以是优先是是因为有一些特殊情况,这些情况下会放到堆上

  • 需要申请的内存过大,超过了栈的存储能力(默认情况栈最大是8M)
  • 编译期间不能确定变量的具体类型(interface类型)

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

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

Go提供了相关的命令,可以查看变量是否发生逃逸 go build -gcflags '-m'

总结

  • Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把内存管理的复杂机制交给编译器。
  • 不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
  • 尽量写出少一些逃逸的代码,提升程序的运行效率。

其他

传值还是传指针的问题:

根据上面的分析,指针更容易出现内存逃逸的现象。而一旦发生了内存逃逸,则不可避免地对GC造成潜在的压力。有种错误的观念:传指针的代价总是比传值的拷贝代价小。这种观念只在像C语言这种没有GC的低级语言中可能适用。原因如下:

  • 对指针解引用的时候,编译器会进行一些检查。
  • 指针一般都不是临近地址的引用,而复制时,一般都是CPU cash中的数据,cash line内的数据的复制,速度基本和一个复制指针相等
    因此,对于小型的数据,一般传值就够了。在某些情况下,需要对代码做一些重构,以消除成员变量中不必要的指针类型。slice有些情况下,可能也会造成内存逃逸,使用已知固定长度的slice,某些情况下会减少内存逃逸。nterface调用方法会发生内存逃逸,某些热点的情况下,可以考虑优化interface的情况。