Golang内存分配逃逸分析

Golang内存分配逃逸分析

Golang 内存分配逃逸分析

总结

内存分配逃逸就是 栈的内存逃逸到堆上,需要 gc 清理,耗性能

如:

  • 指针逃逸在方法内把局部变量指针返回
  • 栈空间不足逃逸(空间开辟过大)
  • 动态类型逃逸(不确定长度大小)(在 interface 类型上调用方法)
  • 发送指针或带有指针的值到 channel 中

参考博客

问题

  • 知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸

    1、golang 的内存分配方式分为**栈(stack)和堆(heap)**两种;栈廉堆贵

    ​ 分配到栈内存的好处:函数返回时会直接释放,不会引起垃圾回收,对性能没有影响

    ​ 分配到堆内存的坏处:会引起 gc 垃圾回收,影响程序性能

    2、其中 而发生内存逃逸是指:如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存不能像栈一样会自动释放,需要 go 本身的 gc 垃圾回收机制释放,会影响程序的运行性能

    3、大白话就是(函数内)(局部)变量的分配从栈跑到堆上

    ## 网上说辞一
    因为如果变量的内存发生逃逸,
    它的生命周期就是不可知的,其会被分配到堆上,
    而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,
    go实现了gc垃圾回收机制,但gc会影响程序运行性能,所以要尽量减少程序的gc操作
    
    ## 网上说辞二
    golang程序变量会携带有一组校验数据,
    用来证明它的整个生命周期是否在运行时完全可知。
    如果变量通过了这些校验,
    它就可以在栈上分配。
    否则就说它 逃逸了,必须在堆上分配。
    

关于堆和栈

  • 先说说 golang 中内存分配方式:

    主要是**堆(heap)栈(stack)**分配两种

    栈分配廉价,堆分配昂贵

    栈分配:对于栈的操作只有入栈和出栈两种指令,属于静态资源分配

    堆分配:堆中分配的空间,在结束使用之后需要垃圾回收器进行闲置空间回收,属于动态资源分配

    使用栈分配:函数的内部中不对外开放的局部变量,只作用于函数中

    使用堆分配:1.函数的内部中对外开放的局部变量

    ​ 2.变量所需内存超出栈所提供最大容量

逃逸场景(什么情况才分配到堆中)

  • 指针逃逸在方法内把局部变量指针返回
  • 栈空间不足逃逸(空间开辟过大)
  • 动态类型逃逸(不确定长度大小)(在 interface 类型上调用方法)
  • 发送指针或带有指针的值到 channel 中

指针逃逸:在方法内把局部变量指针返回

  • 描述

    局部变量原本应该在栈中分配,函数返回后生命周期结束,直接回收,但是由于返回时被外部引用,因此其生命周期大于栈,则溢出,就会分配到堆

  • 举例

    package main
    
    type student struct {
    	Name string
    	Age  int
    }
    
    func escapes1() *student {
    	// 逃逸分析:原stu不是指针,但是返回了&stu;结果:moved to heap(堆): stu
    	// stu:=student{}
    	// return &stu
    	// 逃逸分析:原stu是指针;结果:escapes to heap(堆)
    	//stu := new(student)
    	stu := &student{}
    	return stu
    }
    func main() {
    	escapes1()
    }
    
  • 解决

    返回值类型而不是指针类型即可;将返回值 *student 改为 student

栈空间不足逃逸(空间开辟过大)

  • 描述

    实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

    例如创建一个超大的 slice;是否逃逸取决于栈空间是否足够大

  • 举例

    func escapes2() {
    	//当切片长度和容量扩大到10000时就会逃逸,本机测试8192临界值
    	//是否逃逸取决于栈空间是否足够大
    	s := make([]int, 8191, 8192)
    	for i := range s {
    		s[i] = i
    	}
    }
    func main() {
    	escapes2()
    }
    
  • 解决

    创建适量的 slice

动态类型逃逸(不确定长度大小)(在 interface 类型上调用方法)

  • 描述

    很多函数参数为 interface 类型,比如 fmt.Println(a …interface{})

  • 举例

    func main() {
    	// 在 interface 类型上调用方法
    	a := 1
    	fmt.Println(a)
    	// 堆 动态分配不定空间 逃逸
    	b := 20
    	c := make([]int, 0, b)
    }
    
  • 解决

闭包引用对象逃逸

  • 描述

  • 举例

    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())
    	}
    }
    
  • 解决

发送指针或带有指针的值到 channel 中

  • 描述

    在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。

    所以编译器没法知道变量什么时候才会被释放

  • 举例

  • 解决

在一个切片上存储指针或带指针的值

  • 描述

    一个典型的例子就是 []*string 。这会导致切片的内容逃逸。

    尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。

  • 举例

  • 解决

slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )

  • 描述

    slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。

    如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。

  • 举例

  • 解决

逃逸分析(Escape analysis)

所谓逃逸分析(Escape analysis)是指由编译器决定内存分配的位置,不需要程序员指定。

通过 `go build -gcflags=-m main.go` 查看逃逸的情况
go build -gcflags=-m main.go
# command-line-arguments
./main.go:7:6: can inline foo
./main.go:13:10: inlining call to foo
./main.go:7:10: leaking param: s
./main.go:8:10: new(A) escapes to heap   # 发生逃逸
./main.go:16:13: io.Writer(os.Stdout) escapes to heap
./main.go:16:13: c escapes to heap      # 发生逃逸