Golang 逃逸分析

说明

逃逸分析是指由编译器决定分配的位置,是在栈还中堆上,不需要程序员指定。
函数中申请一个新的对象,分配的内存可能产生如下情况

  • 如果分配在栈中,则在执行结束时可以自动回收
  • 如果分配在堆中,则在执行结束后,由GC处理回收,GC并不会立即回收,其有相关的回收策略

如果要查看程序是的逃逸情况,可以在编译的时候添加参数--gcflags=-m, 查看输入里面是否有escapes to heap的输入,有则是发生逃逸

逃逸策略

  • 如果函数外部没有引用,则优先分配到栈中,这里是优先则是因为栈内存不够的时候,就会放到堆中
  • 如果函数外部有引用,则一定分配到堆中

逃逸场景

指针逃逸

最典型的逃逸的案例,在定义结构体的时候,一般都会使用类似NewClient 这样的形式进行构建一个对象返回,如下:

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)
}

说明:StudentRegister函数中的变量s为其局部变量,但其通过函数返回指针的形式,返回了其指针地址,其指向的就是堆,而不是栈

栈空间不足时,也会发生逃逸

如下代码:

package main
func appendSclice() {
    // 此处申请的空间,在栈中还是够的,所以不会逃逸
    s := make([]int, 1000, 1000)
    // 当把变量s的长度扩大10或更大,就会发生逃逸,如: s := make([]int, 10000, 1000)
    // 这就是因为栈空间不足,导致逃逸,会把内存分配到堆上
    for index:=range s {
        s[index] = index
    }
}
func main () {
    appendSclice()
}

动态类型【interface{}】逃逸

最简单的动态类型逃逸,如下代码:

package main
func main() {
    s := "ken"
    fmt.Println("%v", s)
}

闭包引用的逃逸

经典的 Fibonacci数列算法函数,就是闭包逃逸

func Fibonacci () func () int {
    a, b := 0,1 //函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸
    return func () int {
        a, b = b, a+b
        return a
    }
}

总结

  • 栈上分配的内存比在堆上要效率要高
  • 栈上分配的内存不需要GC处理,在函数执行完成后,就可以直接回收
  • 堆上分配的内存,一定会交给GC处理
  • 逃逸分析的目的是为了确定内存分配的地址是栈上还是堆上
  • 逃逸分析在编译阶段完成

提示

函数的参数,传递指针一定比传值效率高么?
传递指针时,可以减少低层的值拷贝操作,以提交效率,这在数据量比较大的时候,可以考虑,
但是要明白,传递指针是会产生逃逸的,会把变量分配到堆上,而分配到堆上的变量,又是只能通过GC来进来回收,GC在执行时,几乎会暂停所有内存分配操作的,所以在变量比较小的时候,如果传递指针做为参数,反而会加重GC的负担,效率并不会提交