目录

golang内存逃逸

栈和堆

在编译时,一切无法确定大小或大小可以改变的数据,最好放到堆上,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

函数中申请一个新的对象: 如果分配在栈中,则函数执行结束可自动将内存回收; 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理。

逃逸分析基本原则

编译器会根据变量是否被外部引用来决定是否逃逸:

如果函数外部没有引用,则优先放到栈中; 如果函数外部存在引用,则必定放到堆中; 如果栈上放不开,则必定放到堆上;

指针逃逸

我们知道Go可以返回局部变量指针,这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。


package main

type Person struct {
 Name string
 Age  int
}

func PersonRegister(name string, age int) *Person {
 p := new(Person) //局部变量s逃逸到堆

 p.Name = name
 p.Age = age

 return p
}

func main() {
 PersonRegister("微客鸟窝", 18)
}

函数 PersonRegister() 内部 p 为局部变量,其值通过函数返回值返回, p 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析:go build -gcflags=-m

栈空间不足逃逸


package main

func Slice() {
 s := make([]int, 1000, 1000)

 for index, _ := range s {
  s[index] = index
 }
}

func main() {
 Slice()
}

发现并没有发生逃逸。我们把切片长度扩大10倍再试试: s := make([]int, 10000, 10000)

发现当切片长度扩大到10000时就会逃逸。当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

动态类型逃逸

在 Go 中,空接口 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。 因为 fmt.Println() 的参数类型定义为 interface{},因此也发生了逃逸

闭包引用对象逃逸

package main

func main() {
 f := fibonacci()
 for i := 0; i < 10; i++ {
  f()
 }
}
func fibonacci() func() int {
 a, b := 0, 1
 return func() int {
  a, b = b, a+b
  return a
 }
}

Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。