每天一个知识点:Golang 内存逃逸

什么是内存逃逸?

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

逃逸是如何产生的?

如果一个函数返回对一个变量的引用,那么它就会发生逃逸。即任何时候,一个值被分享到函数栈范围之外,它都会在堆上被重新分配。在这里有一个例外,就是如果编译器可以证明在函数返回后不会再被引用的,那么就会分配到栈上,这个证明的过程叫做逃逸分析。

总结:

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

内存逃逸的危害

堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。变量在堆上的分配和回收都比在栈上开销大的多。对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片(采用分区式存储管理的系统,在储存分配过程中产生的、不能供用户作业使用的主存里的小分区称成“内存碎片”。内存碎片分为内部碎片和外部碎片)。

逃逸的例子

  • 向channel发送指针数据。因为在编译时,不知道channel中的数据会被哪个 goroutine 接收,因此编译器没法知道变量什么时候才会被释放,因此只能放入堆中。
package main
func main() {
ch := make(chan int, 1)
x := 5
ch <- x  // x 不发生逃逸,因为只是复制的值
ch1 := make(chan *int, 1)
y := 5
py := &y
ch1 <- py  // y 逃逸,因为 y 地址传入了chan中,编译时无法确定什么时候会被接收,所以也无法在函数返回后回收 y
}
  • 局部变量在函数调用结束后还被其他地方使用,比如函数返回局部变量指针或闭包中引用包外的值。因为变量的生命周期可能会超过函数周期,因此只能放入堆中。
func Foo () func (){
x := 5 // x发生逃逸,因为在Foo调用完成后,被闭包函数用到,还不能回收,只能放到堆上存放
return func () {
x += 1
}
}
func main() {
inner := Foo()
inner()
}
  • 在 slice 或 map 中存储指针。比如 []*string,其后面的数组可能是在栈上分配的,但其引用的值还是在堆上。
package main
func main() {
  var x int
  x = 10
  var ls []*int
  ls = append(ls, &x) // x发生逃逸,ls存储的是指针,所以ls底层的数组虽然在栈存储,但x本身却是逃逸到堆上
}
  • 切片扩容后长度太大,导致栈空间不足,逃逸到堆上。
func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x) // x发生逃逸,ls存储的是指针,所以ls底层的数组虽然在栈存储,但x本身却是逃逸到堆上
}
  • 在 interface 类型上调用方法。 在 interface 类型上调用方法时会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道。
package main
type foo interface {
  fooFunc()
}
type foo1 struct{}
func (f1 foo1) fooFunc() {}
func main() {
var f foo
f = foo1{}
f.fooFunc()  // 调用方法时,f发生逃逸,因为方法是动态分配的
}

如何避免内存逃逸?

  • 对于小型的数据,使用传值而不是传指针,避免内存逃逸。
    • 对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。
    • 对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。
  • 尽量避免使用长度不固定的 slice 切片,因为在编译期无法确定切片长度,只能将切片使用堆分配。
  • 对于性能要求比较高且访问频次比较高的函数调用,谨慎使用 interface 调用方法。

参考

简单聊聊内存逃逸 | 剑指offer - golang
Golang内存逃逸是什么?怎么避免内存逃逸?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 为什么要尽量避免内存逃逸? 因为如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存...
    dongzd阅读 1,334评论 0 1
  • 参考资源一参考资源二参考资源三 对于手动管理内存的语言,比如 C/C++,调用著名的malloc和new函数可以在...
    帘外五更风阅读 1,111评论 0 0
  • 问题 知道golang的内存逃逸吗?什么情况下会发生内存逃逸? 怎么答 golang程序变量会携带有一组校验数据,...
    9號阅读 707评论 0 1
  • 一、类加载机制 类加载就是虚拟机把Class文件加载到内存,并对数据进行校验,解析和初始化,形成可以虚拟机直接使用...
    xiaoqunzi233阅读 299评论 0 0
  • 一、我们说内存逃逸时在说什么 问,内存逃逸是干什么的答,内存逃逸分析是编译器在编译优化时,用来决定变量应该分配在堆...
    银角代王阅读 1,444评论 0 4