逃逸分析(Escape analysis)是指由编译器决定内存分配的位置,不需要程序员指定。函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束可以自动将内存回收。
  • 如果分配在堆中,则函数执行结束可交由GC处理。

逃逸分析使得返回函数局部变量变得可能,并于闭包息息相关

逃逸策略

每当函数中申请新的对象,编译器会根据该对象是否被函数外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中。
  2. 如果函数外部存在引用,则必定放在堆中。

对于函数外部没有引用的对象,也可能放到堆中,比如内存过大超过栈的存储能力

逃逸场景

指针逃逸

Go可以返回局部变量指针,这是一个典型的变量逃逸案例,示例代码如下:

package main
 
type Student struct {
	Name string
	Age int
}
 
func StudentRegister(name string, age int) *Student {
	s := new(Student) // 局部变量s逃逸到堆
	s.Name = name
	s.Age = age
	return s
}
 
func main() {
	StudentRegister("Jim", 18)
}

s为局部变量,其值通过函数返回值返回,s本身为一指针,其指向的内存地址不会是栈而是堆。

栈空间不足逃逸

在栈中申请的局部变量的大小超过了栈空间的大小,那么就会发生逃逸。 例如:

package main
 
func Slice() {
	s := make([]int, 10000, 10000)
	for index, _ := range s {
		s[index] = index
	}
}
 
func main() {
	Slice()
}

通过编译参数-gcflags=-m就可以看到在变量s发生了逃逸。

动态类型逃逸

很多函数参数为interface类型,在编译期间很难确定参数的具体类型,也会产生逃逸。例如:

package main
import "fmt"
func main() {
	s := "Escape"
	fmt.Println(s)
}

s是一个string类型变量,调用fmt.Println()时会产生逃逸。

闭包引用对象逃逸

package main
import "fmt"
 
func Fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}
 
func main() {
	f := Fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Printf("Fibonacci: %d\n", f())
	}
}

由于闭包的原因,使得原来时Fibonacci的局部变量产生了逃逸。

总结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配内存不需要GC处理
  • 堆上分配的内存使用完毕会交给GC处理
  • 逃逸分析目的是决定分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

了解了变量逃逸之后,就可以思考这样一个问题:函数传递指针真的比传值效率高吗?

传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的