golang 内存逃逸 2022-06-09
内存逃逸
在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸
(1)返回函数内局部变量的值:
在C/C++语言中,局部变量分配在栈空间,因为函数返回后,系统会自动回收函数里定义的局部变量,所以在返回局部变量的值时,实际是返回局部变量的副本。
在Go语言中返回局部变量的值也是一样的,返回的也是局部变量的副本。代码如下:
package main
import "fmt"
func foo() int { //int类型函数
tmp := 1
fmt.Println(&tmp) // 0xc00000a0e0
return tmp //返回局部变量
}
func main() {
v := foo()
fmt.Println(&v) // 0xc00000a0c8(和tmp地址不同)
}
(2)返回函数内局部变量的地址
在C/C++语言中,操作函数返回后的局部变量的地址,一定会发生空指针异常。要解决这种问题,只需将内存空间分配在堆中即可。
但在Go语言中,函数内部局部变量,无论是动态new出来的变量还是创建的局部变量,它被分配在堆还是栈,是由编译器做“逃逸分析”之后做出的决定。
关于Go语言的“逃逸分析”,可以参考go FAQ里的讲解,大意如下:
Go编译器在给函数中的局部变量分配空间时会尽可能地分配在栈空间中,但是如果编译器无法证明函数返回后是否还有该变量的引用存在,则编译器为避免悬挂空指针的错误,就会将该局部变量分配在堆空间中;
如果局部变量占用内存很大,Go编译器会认为将其存储在堆空间中更有意义;
Go编译器如果看到了程序中有使用某个变量的地址,则该变量会变成在堆空间上分配内存的候选对象,此时Go编译器会通过分析,判断出该指针的使用会不会超过函数的范围,如果没超过,该变量就可以驻留在栈空间上;如果超过了,就必须将其分配在堆空间中。
对于Go语言中的“逃逸分析”,我们可以看下面的代码:
package main
import "fmt"
func foo() *int { // 返回int类型指针
tmp := 2020
return &tmp // 返回局部变量tmp的地址
}
func main() {
var ptr *int
// main函数中引用了foo函数内的局部变量tmp
// 根据“逃逸分析”,编译器会将其分配在堆空间上
ptr = foo()
// foo函数执行结束后tmp不会被释放
fmt.Println(*ptr) // 结果为2020,不会报错
}
go方法返回struct结构体还是结构体指针?
返回struct结构体会发生浅拷贝,函数内的局部结构体变量分配在栈上,随着方法返回被清理。但浅拷贝的struct结构体依然占有内存
返回结构体指针会让结构体变量逃逸到堆上,堆上的变量常驻时间更长,依赖gc清理
但无论如何,内存中都必然存在一份结构体对象,因此个人认为直接返回结构体指针更加简洁,也避免了浅拷贝
更普遍的说,对象避免非指针返回