什么是内存逃逸?
内存逃逸是一种程序错误,它指程序在运行过程中分配了内存,但是之后没有正确的释放这些被分配的内存,从而导致这些内存一直被占用,这就有可能会引起系统内存耗尽,从而导致程序崩溃。
我们都知道程序运行的时候需要将其拷贝到内存中才能执行,而内存空间划分成了不同的区域,其中最重要的两个区域是:堆区 - Heap 和 栈区 - Stack
为什么要分成堆区和栈区呢?
主要是为了满足不同的编程需求:
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)
}
这段 Go 代码会产生内存逃逸的原因是由于函数 Sum 返回的是指向局部变量 total 的指针,而这个局部变量会在函数退出时被销毁,因此 Go 会将 total 的内存分配(“迁移”)到堆上,从而避免访问已销毁的内存,这种现象就是内存逃逸(如果返回的指针指向了已经销毁的内存,这将导致访问非法内存,为了避免这种问题,Go 会将这个局部变量 total 从栈上“逃逸”到堆上,确保它在 main 函数结束前仍然有效。)。
所以,最简单且常见的做法是应该避免返回局部变量的指针,直接返回值,这样 Go 就无需重新在堆上分配内存,从而避免了内存逃逸。
2、避免使用interface类型作为函数参数,interface在Go中代表任意,也就代表了不确定性。
// 函数PrintValue接收一个类型为 interface{} 的参数,意味着它实际上可以接受任意类型的参数
func printValue(value interface{}) {
fmt.Println(value)
}
func main() {
x := 9
printValue(x)
}
在这个例子中,x 是一个栈上的局部变量,当它被传递给 interface{} 类型的函数时,Go 会将这个值包装成一个 interface{} 类型的对象,interface{} 本质上是一个由类型和值两个部分组成的一个结构。具体是先复制 x 的值 到一个新的内存位置(堆上),因为 interface{} 内部不能直接存储栈上的值;然后保存类型信息,Go 需要知道 x 是 int 类型,以便之后可能进行类型断言。
因此,即使我们传递的是一个简单的 int,Go 也可能需要在堆上为它分配内存,而这种内存分配和复制是额外的开销。
3、避免闭包产生逃逸,闭包会导致内存逃逸的原因是因为闭包函数会捕获并持有外部变量的引用(即指针),如果这些变量是局部变量,且闭包的生命周期比这些变量长,那么就会发生内存逃逸。
func main() {
// 定义一个局部变量x
x := 10
// 定义一个闭包,捕获了外部变量 x
closure := func() int {
return x
}
// 这里闭包函数将被调用,闭包会持有对变量 x 的引用
fmt.Println(closure()) // 输出: 10
}
对于上面的代码,Go 编译器会分析到 closure 函数需要访问外部变量 x,所以会将 x 分配到堆上,而不是栈上,以确保在闭包函数执行时,x 的值是有效的。为了避免闭包产生内存逃逸,可以通过将外部变量的值传递给闭包,而不是捕获它的引用:
func main() {
// 定义一个局部变量x
x := 10
// 定义一个闭包,捕获了外部变量 x
closure := func(y int) int {
return y
}
// 这里闭包函数将被调用,闭包会持有对变量 x 的引用
fmt.Println(closure(x)) // 输出: 10
}