什么是逃逸分析
逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。也就是说通过编译器分析代码的特征和生命周期,决定应该使用堆还是使用栈来进行内存分配。
常见的两种逃逸情景:
- 函数中局部对象指针被返回(不确定被谁访问)
- 对象指针被多个子程序(如线程 协程)共享使用
逃逸分析作用就是优化程序,当函数中生成一个新对象:
- 如果分配到栈上,待函数返回资源就被回收了
- 如果分配到堆上,函数返回后交给gc来管理该对象资源
栈资源的分配及回收速度比堆要快,逃逸分析的最大好处就是减少了GC的压力。
逃逸分析
package main
import "fmt"
func dummy(b int) int {
var c int
c = b
return c
}
func void() {
}
func main() {
var a int
void()
fmt.Print(a, dummy(0))
}
使用go run -gcflags "-m -l" main.go
命令运行代码:
- -gcflags是编译参数
- -m 表示进行内存分配分析
- -l 表示避免程序内联,也就是避免进行程序优化
运行结果如下:
# command-line-arguments
src\basics\main.go:18:11: ... argument does not escape
src\basics\main.go:18:11: a escapes to heap
src\basics\main.go:18:20: dummy(0) escapes to heap
0 0
程序运行结果分析如下:
- 第2行告知“代码的第18行的变量 a 逃逸到堆”。
- 第3行告知“dummy(0) 调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
- 第4行,这句提示是默认的,可以忽略。
上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。变量 c 的值被复制并作为 dummy() 函数的返回值返回,即使变量 c 在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。变量 c 使用栈分配不会影响结果。
取地址发生逃逸
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
var c Data
//返回函数局部变量地址
return &c
}
func main() {
fmt.Println(dummy())
}
使用go run -gcflags "-m -l" main.go
命令运行代码:
# command-line-arguments
src\basics\main.go:10:6: moved to heap: c
src\basics\main.go:16:13: ... argument does not escape
&{}
注意第2行出现了新的提示:将 c 移到堆中。这句话表示,Go编译器已经确认如果将变量c分配在栈上是无法保证程序最终结果的,如果这样做,dummy() 函数的返回值将是一个不可预知的内存地址,这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址。
Go语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。
总结
逃逸分析是编译器在静态编译的时候,分析对象的生命周期及引用情况来决定对象内存分配到堆上还是栈上,由于栈内存分配较堆快且栈内存回收较快(无需gc),编译器以此来优化程序性能。