内存逃逸
在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸
函数的返回动作都是值拷贝,将返回值以值传递方式赋值给调用方
(1)返回函数内局部变量的值本身:
在C/C++语言中,局部变量分配在栈空间,函数返回后,系统会自动回收函数里定义的局部变量,所以在返回局部变量的值时,实际是返回局部变量的副本,即返回另一个被返回值赋值的临时变量。
在Go语言中返回局部变量的值也是一样的,另一个被返回值赋值的临时变量。代码如下:
package main
import "fmt"
func foo() int { //int类型函数
tmp := 1
fmt.Println(&tmp) // 0xc00000a0e0
return tmp //返回局部变量
}
func main() {
v := foo() // 实际上是foo把tmp的副本的值赋给了v,值传递
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清理
但无论如何,内存中都必然存在一份结构体对象,因此个人认为直接返回结构体指针更加简洁,也避免了浅拷贝
更普遍的说,对象避免非指针返回
会发生逃逸分析的场景基本上有以下几点共性特点
变量在函数返回后,其他函数中仍可以被用到。 其实就是指指针(或者其他可以退化为指针的结构体)泄露到外部,比如函数返回指针、闭包等
无法在编译时期知道准确的大小,多存在于动态增长结构、interface{}或接口实现
变量超过栈大小上限,虽然go的栈会动态增长,但是里面也有一个限制初始分配的阈值,如果超过这个阈值则直接分配到堆