likes
comments
collection
share

一文详解Go内存分配与逃逸分析

作者站长头像
站长
· 阅读数 9

前言

逃逸分析,简单的说就是确定内存是分配到栈上还是堆上

将内存分配到栈上的性能会比分配到堆上的性能明显要高,Go在编译阶段就会完成逃逸分析,编译器会尽可能的将内存分配到栈上,但当检查到变量在生命周期内发生未知的情况,则会产生逃逸现象,从而会将变量的内存分配到堆上。

插曲:为什么将内存分配到栈上的性能会比分配到堆上的性能明显要高呢?

原因是因为栈的内存是由编译器自动进行分配和释放的,非常高效,它们随着函数的创建而分配,随着函数的退出而销毁;而对于堆上的内存回收,得通过Go的三色标记法,进行垃圾回收,并且有时还需要加锁来防止多线程冲突。

逃逸分析原则

  • 编译阶段无法确定的参数,会逃逸到堆上;
  • 变量在函数外部存在引用,会逃逸到堆上;不存在引用,则会继续在栈上;
  • 变量占用内存较大时,会逃逸到堆上;

逃逸分析举例

下面通过举例,来进一步论证逃逸分析的原则,加深一下理解

我们可以使用这个命令go build -gcflags '-m -m -l' go文件名,来查看逃逸分析的结果

1.参数为interface类型会逃逸

func main() {
  num := 1
  fmt.Println(num)
}

运行结果:

一文详解Go内存分配与逃逸分析

原因分析:

func Println(a ...interface{}) (n int, err error),这个函数的入参是interface类型,编译阶段无法确定其具体的参数类型,所以内存分配到堆上

2.变量在函数外部有引用会逃逸

func main() {
  _ = test()
}

func test() *int {
  num := 10
  return &num
}

运行结果:

一文详解Go内存分配与逃逸分析

原因分析:

变量num在函数外部存在引用,函数退出时栈中的内存(栈帧)已经释放,但引用已经被返回,如果通过引用地址取值,在栈中是取不到值的,所以Go为了避免这个情况,会将内存分配到堆上

3.变量占用内存较大时会逃逸

func main() {
  //不会逃逸
  s1 := make([]int, 10, 10)
  for i := 0; i < 10; i++ {
    s1[i] = i
  }

  //会逃逸
  s2 := make([]int, 10000, 10000)
  for i := 0; i < 10000; i++ {
    s2[i] = i
  }
}

运行结果:

一文详解Go内存分配与逃逸分析

原因分析:

切片容量过大时,会产生逃逸,内存分配到堆上;容量小时,不会逃逸,内存分配依赖在栈上

4.变量大小不确定时会逃逸

func main() {
  num := 10
  s := make([]int, num, num) 
  for i := 0; i < num; i++ {
    s[i] = i
  }
}

运行结果:

一文详解Go内存分配与逃逸分析

原因分析:

切片的长度和容量,虽然通过声明的变量num来指定了,但在编译阶段是未知的,并不确定num的具体值,所以会逃逸,将内存分配到堆上

应用实践

通过上面的举例,更加清晰的知道了哪些情况会产生逃逸,哪些不会产生逃逸。知道了这些,才能写出更优质的代码。

实践案例:方法接收者变量,什么时候用结构体,什么时候用结构体指针呢?

分情况讨论:

  • 结构体较大时:如果使用结构体,虽然分配在栈上不逃逸,但会产生值拷贝,带来的是更多的性能开销与内存占用,所以使用结构体指针更加合适,产生了逃逸但节省了内存空间,同时也避免了额外的性能开销;
  • 结构体较小时:这种情况就适合使用结构体,分配在栈上,减少了GC压力的同时提高了性能;

总结

内存分配到栈上还是堆上,通过逃逸分析就能明确知道。基于逃逸分析的原则,使用举例来做了论证,加深了理解与使用。

弄清楚了逃逸分析,结合实际应用场景,我们才能写出更优质的代码,要尽可能的让内存分配到栈上,减少GC压力以提高性能,但并不绝对,额外的性能开销与内存空间的高占用,我们也得选择更适当的平衡点

如果本文对你有帮助,欢迎点赞收藏加关注,如果本文有错误的地方,欢迎指出!