什么是内存逃逸?

内存逃逸是一种程序错误,它指程序在运行过程中分配了内存,但是之后没有正确的释放这些被分配的内存,从而导致这些内存一直被占用,这就有可能会引起系统内存耗尽,从而导致程序崩溃。

我们都知道程序运行的时候需要将其拷贝到内存中才能执行,而内存空间划分成了不同的区域,其中最重要的两个区域是:堆区 - Heap 和 栈区 - Stack

Stack栈区和Heap堆区

为什么要分成堆区和栈区呢?

主要是为了满足不同的编程需求:

1、效率:栈区自动管理,适合快速的内存分配和释放,而堆区则适合存储需要长时间存在的大量数据。

2、灵活性:堆区基本可以随心所欲动态的分配内存,适合需要灵活使用大块内存的情况,而栈区刚好相反适合局部短时间使用的情况。

3、内存管理:有助于更好的管理和优化内存使用,提高程序的性能和稳定性。

栈区:用来存放函数调用时的局部变量和函数调用信息的;一般是由系统自动管理的,当函数调用开始时分配内存,函数调用结束时自动释放内存;栈区的分配和释放速度都非常快;栈区一般是有大小限制的,所以如果函数嵌套调用过多,或者局部变量太多也可能会导致栈溢出。

堆区:用于存储动态分配的内存(比如使用new或者malloc等函数分配的内存);特别是C语言中是需要一由程序员手动管理分配和释放的;因为是手动管理分配和释放,所以速度一般比较慢;堆区的大小一般来说比栈区大,可以存储更多的数据。

分配快慢的问题:

栈分配内存只需要用到两个CPU指令,即PUSH分配和RELEASE释放;

堆分配内存首先需要找到一块大小合适的内存块,之后还要通过垃圾回收才能释放。

Go语言为了解决内存逃逸实现了一套内存逃逸分析,所谓逃逸分析就是指程序在编译阶段对代码中哪些变量需要在栈中分配,哪些变量需要在堆上分配进行静态分析的方法。逃逸分析的目的是做到更好内存释放分配,提高程序的运行效率。

Go逃逸分析的源码位于src/cmd/compile/internal/gc/escape.go,感兴趣的可以自行研究,其逃逸分析原理在源的注释中已有说明:

pointers to stack objects cannot be stored in the heap - 指向栈对象的指针不能存储在堆中

pointers to a stack object cannot outlive that object - 指向栈对象的指针不能超过该对象的生命周期

知道了以上知识点,我们再看如何避免Go内存逃逸?

1、避免返回函数内部的局部指针变量,比如下面这段代码就会产生内存逃逸:

func Sum(a int, b int) *int {
	total := a + b
	return &total
}

func main() {
	Sum(200, 50)
}

2、避免使用interface类型作为函数参数,interface在Go中代表任意,也就代表了不确定性。

3、避免闭包产生逃逸,因为闭包函数也是一个指针。