内存逃逸分析是 Go 语言中的一个重要概念。
在了解内存逃逸之前,需要先知道栈内存和堆内存的概念:
堆内存(Heap):一般由人为手动进行管理,手动申请、分配、释放。其大小一般取决于硬件内存,适合不可预知大小的内存分配,但分配速度较慢,且可能会形成内存碎片。
栈内存(Stack):是一种具有特殊规则的线性表数据结构,由编译器自动进行管理,自动申请、分配、释放。其大小通常是固定的。
Go 编译器会通过逃逸分析来决定变量分配在栈上还是堆上。逃逸分析是用于堆和栈分配的选择,编译器会追踪变量在代码块的作用域,判断变量在整个运行周期是否完全可知,若可以则在栈上分配;否则逃逸到堆上。
Go 语言工具链提供了查看对象是否逃逸的方法。在执行go build
时,可以配合使用参数-gcflags
开启编译器支持的额外功能。例如,使用go build -gcflags '-m -l' main.go
,其中-m
会打印出逃逸分析的优化策略,-l
会禁用函数内联,以更好地观察逃逸情况,减少干扰。
此外,还可以通过反编译命令go tool compile -S main.go
来更底层、更准确地判断一个对象是否逃逸。
一些常见的导致内存逃逸的情景包括:
在方法内把局部变量指针返回:局部变量原本应在栈中分配和回收,但由于返回时被外部引用,其生命周期大于栈,从而发生逃逸。
func getPointer() *int {
num := 10
return &num
}
在上述代码中,函数getPointer
返回了局部变量num
的指针,由于函数返回后局部变量num
的生命周期并未结束,所以num
会逃逸到堆上
在切片上存储指针或带指针的值:例如
(*string)
,这会导致切片的内容逃逸,尽管后面的数组可能在栈上分配,但其引用的值一定在堆上。如果切片的背后数组因append
操作可能超出其容量而被重新分配,也会在堆上分配。
func expandSlice() {
s := make([]int, 0, 1)
for i := 0; i < 10; i++ {
s = append(s, i)
}
}
当切片不断扩容,可能会导致底层数组重新分配内存,从而使元素发生逃逸。
func closureExample() {
num := 10
f := func() {
fmt.Println(num)
}
f()
}
闭包中引用的外部变量可能会逃逸到堆上。
在
interface
类型上调用方法:由于方法的真正实现只能在运行时知道,这是动态调度,所以也可能发生逃逸。
func interfaceExample() {
num := 10
var i interface{} = num
}
当变量被赋值给 interface{}
类型时,可能会发生逃逸。
大尺寸的结构体或数组
如果在函数内部创建了较大尺寸的结构体或数组,且其使用超出了函数的作用域,也可能发生逃逸。逃逸分
析内存逃逸分内存逃逸分析析