轻松理解Go中的内存逃逸问题

2023-06-27 02:36:46 浏览数 (2)

内存逃逸是什么

  1. 在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式,寻址起来十分迅速,开销很少。这一块内存地址称为栈。
  2. 栈是线程级别的,大小在创建的时候已经确定,当变量太大的时候,会"逃逸"到堆上,这种现象称为内存逃逸。
  3. 简单来说,局部变量通过堆分配和回收,就叫内存逃逸。

内存逃逸危害

  1. 堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。
  2. 全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。
  3. 变量在堆上的分配和回收都比在栈上开销大的多。
  4. 对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片。

内存逃逸现象

  1. 向 channel 发送指针数据。因为在编译时,不知道channel中的数据会被哪个 goroutine 接收,因此编译器没法知道变量什么时候才会被释放,因此只能放入堆中。
  2. 局部变量在函数调用结束后还被其他地方使用,比如函数返回局部变量指针或闭包中引用包外的值。因为变量的生命周期可能会超过函数周期,因此只能放入堆中。
  3. 在 slice 或 map 中存储指针。比如 []*string,其后面的数组可能是在栈上分配的,但其引用的值还是在堆上。
  4. 切片扩容后长度太大,导致栈空间不足,逃逸到堆上。
  5. 在 interface 类型上调用方法。 在 interface 类型上调用方法时会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道。

逃逸分析原则

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

内存逃逸解决

  1. 对于小型的数据,使用传值而不是传指针,避免内存逃逸。
  2. 避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配。
  3. interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用。 避免内存逃逸需要遵循如下两个原则:
  4. 指向栈对象上的指针不能被存储到堆中。
  5. 指向栈对象上的指针不能超过该栈对象的声明周期。

具体案例

参数为interface类型会逃逸

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

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

代码语言:go复制
func main() {
  num := 1
  fmt.Println(num)
}

原因分析:

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

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

代码语言:go复制
func main() {
  _ = test()
}

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

原因分析:

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

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

代码语言:go复制
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复制
func main() {
  num := 10
  s := make([]int, num, num) 
  for i := 0; i < num; i   {
    s[i] = i
  }
}

原因分析:

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

0 人点赞