golang 内存分配

477 阅读4分钟

new出来的变量是分配在栈中还是分配在堆中

new出来的变量需要根据逃逸分析后才能决定变量分配在栈中还是堆中。

  • new函数作用:返回参数类型指针,并指向新分配类型的地址
  • make函数作用: 只用于切片,map,channel的内存分配,并为引用类型初始化

make和new的区别与相同

  • 对于make和new分配的内存,go编译器尽量将变量分配在栈上,如果变量未发送内存逃逸,那么就会在栈上分配,否则分配在堆上
  • 如果变量占用内存很大超过了栈空间,或者栈空间不足,那么就会分配在堆上。
  • make(slice,n)对于切片的容量大小不固定,其会在堆上分配。

不同:

  • make函数只用于slice,map以及channel的初始化
  • new用于类型内存分配,返回一个函数指针。

逃逸分析

在C语言和C++中,我们可以手动分配内存,也就是我们可以决定一个对象或者结构体分配到栈上或者是堆上。但是手动分配内存会存在两个问题:

  • 不需要分配到堆上的对象分配到了堆上导致浪费内存空间
  • 需要分配到堆上的对象分配到了栈上导致悬挂指针,影响内存安全 在golang中,逃逸分析是用来决定指针动态作用域的方法。Go语言的编译器使用逃逸分析决定哪些变量在栈上分配,哪些变量在堆上分配。逃逸分析遵循以下两个规则:
  • 指向栈对象的指针不能分配在堆中
  • 指向栈对象的指针不能在栈对象回收后存活 注意: 无法明确变量生命周期的对象会被放置在堆上,这样可以最大程度保证了代码的安全性。 函数传递指针真的比传值效率高吗? 我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

逃逸分析过程

在编译器解析了Go语言源文件后,它可以获得整个程序的抽象语言树,编译器可以根据抽象语法树分析静态的数据流。具体过程如下:

  • 构建带权重的有向图,顶点表示被分配的变量,边表示变量之间的分配关系,权重表示寻址和取地址的次数。
  • 遍历对象分配图并查找违反两条不变性的变量分配关系,如果堆上的变量指向了栈上的变量,那么该变量就需要逃逸到堆中。
  • 为了支持函数间的逃逸分析,算法还会记录从函数的调用参数到堆以及返回值的数据流,增强函数参数的逃逸分析

逃逸分析的典型情况

  • 函数方法返回局部变量的指针,导致局部变量逃逸到堆中。
  • 发送指针或带有指针的结构体到channel中,在编译时,我们没有办法知道哪个goroutine会接收数据,所以编译器不知道变量什么时候会释放
  • 在一个切片上存储指针或带指针的值。底层数组可能会在栈上分配,但是引用指针一定会在堆上分配
  • slice初始化的时候有可能会在栈上分配,但是当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。slice使用基于运行时的数据扩容时也会在堆上分配。
  • 在interface类型上调用方法,比如fmt.Println(interface{}),无论interface类型是什么,该参数都会分配在堆上。

如何避免逃逸

使用noescape函数遮蔽输入和输出的依赖关系,使编译器不认为该变量会逃逸。noescape函数在runtime包中使用unsafe.Pointer的地方被大量使用。