你必须掌握 Golang 的内存逃逸知识点
最近,一位朋友在学习 Golang 时遇到了一个问题,以下代码实际上并没有报错,而他在写 C 语言时,这段代码不应该被编译通过。
func demo() []int {
data := make([]int, 1000)
return data[:900]
}
这段代码对于 Golang 来说确实没有问题,我们来看看下面的 C 代码。
int * demo() {
int data[1000];
return data;
}
当我们执行 gcc demo.c
时,会返回一个警告。
demo.c:6:13: warning: address of stack memory associated with local variable 'data' returned [-Wreturn-stack-address]return &data;1 warning generated.
提示说返回了栈内存,这是不被允许的。朋友很好奇为什么 Golang 中允许这种行为,这就引发了我们今天要分析的问题:Golang 的内存逃逸行为。
程序中的内存分为两个区域,一个是栈,一个是堆。栈区具有特定的结构和寻址方式,并且寻址速度非常快,开销很小。而堆则是一块没有特定结构和固定大小的内存区域,可以根据需要进行调整。
全局变量、占用大内存的局部变量以及函数调用结束后不能立即回收的局部变量,都存储在堆内。 变量在堆上的分配和回收比在栈上的开销要大得多。
简单来说,函数调用内部申请的内存存储在栈上,这些内存在函数结束时立即返回给系统。
在上面的例子中,函数内申请的变量 data
在函数结束后会被回收并且不会持久存在。它们都存储在栈区。因为存在时间短,而且栈区分配速度快,编译器会确认这是最佳选择。
再来看一个例子。
在上面的例子中,变量 data
在函数结束后不会被释放,因为返回了变量的指针,编译器会认为变量在函数结束后还在其他地方被使用,所以会保留这个内存,并同时申请到堆上,因为堆内存不会立即回收,而栈内存会立即回收。
我们使用 go tool compile -m
来分析原始代码:
$ go tool compile -m demo.godemo.go:3:6: can inline demoFunctiondemo.go:8:6: can inline maindemo.go:9:31: inlining call to demoFunctiondemo.go:4:9: moved to heap: data
根据分析结果,我们可以看到 moved to heap: data
表示 data
变量已经逃逸到了堆内存。
Golang 官网 FAQ 中有一个关于变量赋值的说明:
从正确性的角度来看,你不需要知道。每个 Go 中的变量只要有引用就存在。实现选择的存储位置与语言的语义无关。
存储位置确实会影响编写高效程序。在可能的情况下,Go 编译器将在该函数的堆栈帧中分配局部函数的变量。
但是,如果编译器无法证明变量在函数返回后不再被引用,则编译器必须在垃圾收集的堆上分配变量,以避免悬空指针错误。此外,如果局部变量非常大,将其存储在堆上而不是堆栈上可能更有意义。
在当前的编译器中,如果变量的地址被取出,那么该变量是堆上分配的候选变量。但是,基本逃逸分析会识别出一些情况,当这些变量从函数返回时不会存在,并且可以驻留在堆栈上。
总结一下:如果一个变量被取地址,那么它可能会被分配到堆上。但是,这些只能在对变量执行逃逸分析操作后确认。如果变量在函数返回后不再被引用,它将被分配到栈上。
让我们看看内存逃逸的场景。
1. 指针逃逸
变量 p
是一个局部变量,但是因为它被引用返回,所以会逃逸到堆内存。编译分析如下:
$ go tool compile -m person.goperson.go:7:6: can inline SetPersonperson.go:13:6: can inline mainperson.go:14:11: inlining call to SetPersonperson.go:7:16: leaking param: nameperson.go:8:10: new(Person) escapes to heap // [Escape happens here]person.go:14:11: new(Person) does not escape
2. 栈空间不足以逃逸
分析如下:
$ go tool compile -m index.goindex.go:4:11: make([]int, 10000, 10000) escapes to heap
3. 动态类型逃逸
有许多函数的参数类型为 interface
类型,在编译时很难确定参数的具体类型,这种类型的情况也会产生逃逸。例如内置函数 Println
方法。
分析如下:
$ go tool compile -m hello.gohello.go:5:6: can inline mainhello.go:6:13: inlining call to fmt.Printlnhello.go:6:14: "hello golang" escapes to heap // [Escape happens here]hello.go:6:13: []interface {}{...} does not escape<autogenerated>:1: leaking param content: .this
在程序编译时会进行内存逃逸分析,内存逃逸分析有 2 个好处:
- 逃逸分析后,可以确认特定变量分配在堆内存还是栈内存,提高程序性能。
- 减少 GC 压力,不逃逸的变量可以及时回收。
最终总结:
- 对于小数据使用传值而不是指针
- 避免使用长度不确定的
slice
存储热数据 - 避免将参数传递给
interface
,尽量使用明确的类型 - 变量的分配在逃逸分析后确认。Golang 官方这样明确是为了让开发者更专注于业务逻辑本身,而不用太过关注内存问题。
译自:https://blog.devgenius.io/in-depth-analysis-of-golang-memory-escape-edfbfb856913
评论(0)