大家好,我是公号「左诗右码」作者 Alex。
在 C/C++ 时代,程序员必须手动决定对象分配在堆上(malloc)还是栈上(局部变量)。如果把一个栈上分配的变量指针返回出去,函数结束时栈帧被销毁,这个指针就变成了“野指针”,会导致灾难性的后果。
但在 Go 语言里,你只需要 new 或者 & 取地址,Go 编译器会在后台默默帮你做决定:这个变量到底该放在堆上还是栈上?
这个后台做决定的过程,就叫逃逸分析(Escape Analysis)。
1. 什么是 Golang 的内存逃逸?
内存逃逸指的是:一个本该分配在栈(Stack)上的局部变量,因为生命周期超出了当前函数的范围,或者大小/类型不确定,被编译器“偷偷”转移分配到了堆(Heap)上。
为什么我们要关心逃逸?
- 栈分配:极快!只需要移动栈指针,函数返回时自动回收,完全没有 GC 压力。
-
堆分配:较慢!需要走上一篇讲的
mcache/mcentral分配流程,且最后必须由 GC 来回收,增加了 GC 的负担。
所以,减少内存逃逸,是 Go 性能优化的核心手段之一。
2. 逃逸分析是怎么进行的?
Go 的逃逸分析是在编译阶段完成的。编译器通过构建整个程序的抽象语法树(AST)和静态单赋值(SSA),追踪每个变量的作用域和引用关系。
核心原则只有两条:
- 指向栈对象的指针不能存在于堆中。
- 指向栈对象的指针不能在栈对象回收后依然存活。
你可以通过在编译时加上 -gcflags="-m" 参数,让编译器打印出逃逸分析的详细过程。
go build -gcflags="-m" main.go
3. 什么情况下会发生内存逃逸?
面试时,面试官最喜欢让你举几个逃逸的例子。记住以下这 4 种最常见的场景:
3.1 指针逃逸(返回局部变量的指针)
这是最典型的逃逸。我们在写代码时,为了避免结构体拷贝带来的性能损耗,通常会选择传递结构体指针(顺带一提,Go 语言中所有的函数参数传递都是值传递,即使你传的是指针,传的也是指针地址的拷贝)。
但是,当你把局部变量的指针返回到函数外部时,这个局部变量 x 本该在函数退出时销毁,但因为外部还在用它,所以它只能逃逸到堆上。
func createInt() *int {
x := 1 // x 逃逸到堆上
return &x
}
3.2 动态类型逃逸(interface{})
当你把一个具体类型赋值给 interface{} 时,由于编译器在编译期无法确定它的确切大小和内部结构,只能将其放到堆上。
最常见的受害者:fmt.Println()。
func printVal() {
x := 10 // x 逃逸到堆上,因为 fmt.Println 的参数是 ...interface{}
fmt.Println(x)
}
3.3 空间不足或大小不确定的逃逸
栈空间是有限的(通常初始只有 2KB)。
- 如果分配的对象太大,会直接分配到堆上。
- 如果在编译期无法确定切片的大小(如使用了变量作为长度),也会逃逸到堆上。
func makeSlice() {
n := 10
s := make([]int, n) // n 是变量,编译期未知大小,s 逃逸到堆上
}
3.4 闭包引用逃逸
当闭包内部引用了外部函数的局部变量时,这个局部变量就会逃逸到堆上,因为它的生命周期要和闭包绑定在一起。
func counter() func() int {
count := 0 // count 逃逸到堆上
return func() int {
count++
return count
}
}
4. Channel 分配在栈上还是堆上?
这是一个非常隐蔽的面试题。
结论:make(chan T) 创建的 Channel 底层结构体 hchan 一定分配在堆上。
原因:Channel 的主要作用是在不同的 Goroutine 之间传递数据。这就意味着,Channel 必然会被多个 Goroutine 共享。只要被跨 Goroutine 共享,它的生命周期就超出了当前函数的栈帧,因此必然发生逃逸,分配在堆上。
5. 总结:哪些对象在堆上?哪些在栈上?
-
在栈上:生命周期完全局限在当前函数内,大小在编译期确定,且没有被作为
interface{}传递的局部变量。 -
在堆上:
- 被返回出去的局部变量指针。
- 被赋值给
interface{}的变量(如fmt.Println参数)。 - 编译期大小不确定的切片/对象。
- 被闭包引用的局部变量。
- 跨 Goroutine 共享的对象(如 Channel、放入 Channel 的数据、跨协程的锁)。
了解了逃逸分析,你就能写出 GC 压力更小的高性能代码。
但如果在运行过程中,有些应该被回收的堆对象一直无法被回收,那就会导致内存泄漏。
下一篇,我们就来聊聊:内存泄漏排查指南:Goroutine 泄漏怎么办?