Go语言自习室之逃逸分析

206 阅读5分钟

在Go语言中,程序们几乎不需要再担心内存泄漏了。这是因为在Go语言中,堆和栈的区别被模糊化。一个变量是在堆上分配还是在栈上分配,这都是经过编译器的逃逸分析之后得出的结论。

什么是逃逸分析

在编译原理中,分析指针动态范围的方法被称之为逃逸分析。通俗的说,一个对象的指针被多个方法或线程引用时,则称这个指针发生了逃逸。逃逸分析决定了一个变量是分配在堆上还是在栈上。

逃逸分析的作用

若变量都分配在堆上,堆不像栈一样可以自动清理。这样会引起 Go 的 GC 的频繁进行,因而占用较大的系统开销。并且栈与堆相比,栈内存的分配比起堆要快上很多。栈的内存分配只需要调用 PUSH 指令即可,并且会主动释放。而堆也有堆的好处,虽然堆的分配速度较慢,并且会出现内存碎片,而且堆上资源的释放还得依靠 GC 回收,但是对于未知大小的内存分配来说,堆还是比较适合的。

而通过逃逸分析,编译器尽量把那些不需要分配到堆上的变量直接分配到栈上,不仅减少了堆的使用,减轻内存分配的开销,再者也减少了 GC 的压力,提高整个程序的运行速度。

怎样进行逃逸分析

逃逸分析最基础的原则就是判断一个函数是否返回对一个变量的引用。若有,则这个变量就会发生逃逸。

Go语言中没有一个关键词或者函数可以直接让变量分配在堆上,这一切都是编译器通过分析代码得到的。

因此,编译器通过以下两点决定变量是否逃逸:

  1. 如果变量在函数外部没有引用,则优先分配到栈上。
  2. 如果变量在函数外部存在引用,则必定分配到堆上。 关于第一点的优先一说,是取决于变量大小的。若定义了一个很大的变量(需要的内存大小超过了栈的存储能力时),即使函数外部没有引用,编译器也会将其分配至堆上。

怎样查看是否发生逃逸

Go语言中对于逃逸提供了相关的命令,下面有一段示例代码:

package main

import "fmt"

func escape() *int {
   e := 2
   return &e
}

func main() {
   s := escape()
   fmt.Println(*s)
}

escape 函数返回一个局部变量的指针,然后用在 main 函数进行引用并用 s 接收它,因而将会发生逃逸。下面执行以下命令:

go build -gcflags '-m -l' main.go

其中 -gcflags 表示启动编译器支持,-m 用户输出编译器的优化细节, -l 表示禁用 escape 函数的内联优化,防止发生的逃逸被编译器通过内联优化抹除了。得到以下输出:

# command-line-arguments
.\main.go:6:2: moved to heap: e
.\main.go:12:13: ... argument does not escape
.\main.go:12:14: *s escapes to heap

可以发现,escape 函数中的 e 变量 move to heap ,也就是说移动到堆上分配了,所以和预想的一致。而至于 main 函数中的s也发生了逃逸,是因为有些函数的参数为 interface 类型,比如说 fmt.Printf(a ...interface{}),编译期间很难确定参数的具体类型,也会发生逃逸。

通过反汇编也能查看变量是否发生了逃逸。执行以下命令:

go tool compile -S main.go

可以看到,有一行中出现这样一行输出

 0x0020 00032 (main.go:6)        CALL     runtime.newobject(SB)

而其中 runtime.newobject 函数用于在堆上分配一块内存,说明 e 被分配至堆上,也就是说发生了逃逸。

* 对比Go与C/C++中堆栈的概念区别

C/C++中的程序堆栈本质上其实是操作系统层面的概念,程序启动时会自动维护一个所启动程序消耗内存的地址空间,并自动将这个空间从逻辑上划分成堆空间和栈空间。

而 Go 中的区别在于,在 Go 语言运行是,传统意义上的栈空间就已经被全部消耗了,用于维护各个组件之间的协调,例如调度器、垃圾回收、系统调用等,因此对于用户态来说,他们所消耗的“堆和栈”,其实是Go在运行时向操作系统申请而来的堆内存,构成逻辑上的“堆和栈”。Go 用户态的“栈”相比于只有 1MB 的 C/C++ 中的“栈”而言,他是“几乎无限的”(1GB)。而同时,为了防止内存碎片化,Go运行时会在适当的时候对整个栈进行深拷贝,将其复制到另一块内存区域。当然,这一切对于用户来说是透明的。这也是相较于传统意义上的栈是一块固定分配好的内存所出现的另一处差异。也正因此,指针的算术运算不再能奏效。因为在没有特殊说明的情况下,用户无法确定运算前后所指向的地址的内容是否已经被 Go 运行时移动了。