浅谈内存逃逸——以GO语言为例

1,221 阅读4分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

什么是内存逃逸?

废话不多说,上货。 在这里插入图片描述

追根溯源

生命不息,学习不止,总能发现新玩意。我们都知道当我们创建对象时,会自动在内存中分配一块区域用来存放对象实例。 那你们听说过内存逃逸这个说法嘛? 是谁发生了逃逸? 又逃到了那里去? 一起来看看吧。 在这里插入图片描述

何为内存逃逸

内存逃逸是指内存分配中,变量分配时,编译器会根据对象,变量和方法的调用情况进行逃逸分析。 内存逃逸这种现象在很多语言中都有发生,类似java会在JVM发生内存逃逸,包括方法逃逸和线程逃逸,同样在go语言中也有内存逃逸的现象发生,今天我们就以go为例说一说内存逃逸。

内存分配一般有两种情况,一种是在栈上分配,一种是在堆上分配。内存逃逸就是指当内存分配完毕后,编译器会根据对象,变量和方法的调用情况进行逃逸分析后,内存分配情况发生了改变,例如从栈逃逸到了堆,或从堆逃逸到了栈。 文字终究略显空洞,上代码

写个demo

package main

import "fmt"

func demo(user int) int {
    //声名一个变量并赋值
     var admin int 
     
     admin = user

     return admin
}
//空的没啥用的方法
func void() {
}


func main() {
   // 声明user变量并打印
    var user int 

    void()

    fmt.Println(user, demo(0))

}

接着使用如下命令行运行上面的代码:

go run -gcflags "-m -l" main.go

使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。

运行结果

# command-line-arguments
./main.go:29:13: user escapes to heap
./main.go:29:22: demo(0) escapes to heap
./main.go:29:13: main ... argument does not escape
0 0

./main.go:29:13: a escapes to heap 看到这句了嘛,告诉了你变量uesr逃逸到了 堆区 ./main.go:29:22: demo(0) escapes to heap 同样,demo(0)也逃逸到了heap区

逃逸分析

现在就来说说编译器判定逃逸的规则是什么? 引用一段似乎是来自官方翻译的一段话

如果一个函数返回对一个变量的引用,那么它就会发生逃逸。任何时候,一个值被分享到函数栈帧范围之外,它都会在堆上被重新分配。如果函数return之后,确定变量不再被引用,则将其分配到栈上。然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

总结一下就是 如果函数外部没有引用,则优先放到栈中; 如果函数外部存在引用,则必定放到堆中; 如果栈上放不开,则必定放到堆上

常见情景

  • 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
  • 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
  • slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
  • 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。

最后给你们推荐一本书吧

在这里插入图片描述

如果有错误或者需要补充请写在下面,跟我打一架! 在这里插入图片描述 古德拜~