什么叫内存逃逸
函数内部的变量不对外开放的局部变量,只作用在当前函数中,他的内存是分配在栈中的,执行函数的时候会进入栈中,函数结束后会出栈,同时释放内存,当某些变量在执行完后没是释放内存,这种情况被称为内存逃逸。会导致内存回收需要GC。
引起内存逃逸的关键就是,编译器在编译的时候无法确定确定变量的生命周期,只能在运行时控制了。
什么时候会造成内存逃逸
- 盲目使用变量指针作为参数
- slice的长度超出容量重新分配(切片扩容)
- 在切片上存储指针或带有指针的值
- 函数返回了局部变量的指针
- 发送指针或者带有指针的数据进channel
- 调用interface的方法(fmt.println)
- 函数的参数为 interface类型,编译期间很难确定参数的具体类型
- 堆上动态分配内存比栈上静态分配内存,开销大很多
逃逸分析是什么
它是编译器执行静态代码分析后,对内存管理进行的优化和简化。
当一个对象的指针被多个方法或线程引用时,则称这个指针发生了逃逸。逃逸分析决定一个变量是分配在堆上还是分配在栈上。
逃逸分析有什么作用
如果变量都分配到堆上,堆不像栈可以自动清理。就会引起Go频繁的进行垃圾回收,而垃圾回收会占用比较大的系统开销。
通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻堆内存分配的开销,同时也会减少垃圾回收(GC)的压力,提高程序的运行速度。
逃逸分析是怎么完成的
如果一个函数返回对一个变量的引用,那么这个变量就会发生逃逸。
编译器会分析代码的特征和代码的生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。
- 如果变量在函数外部没有引用,则优先放到栈上。
- 如果变量在函数外部存在引用,则必定放到堆上。
- 如果定义了一个很大的数组,需要申请的内存过大,超过了栈的存储能力,也会被分配到堆上。
如何确定是否发生了逃逸分析
go build -gcflags '-m -l' main.go
-gcflags 用于启用编译器支持的额外标志
-m 用于输出编译器的优化细节(包括逃逸分析这种优化)
-N 用于关闭编译器优化
-l 用于禁用函数的内联优化,防止逃逸被编译器通过内联彻底的抹除
Go与C/C++中的堆和栈是同一个概念吗
C/C++中提及的 “程序堆栈” 本质上其实是操作系统层级的概念,它通过C/C++语言编译器和所在的系统环境来共同决定。
C/C++中声明一个局部变量,则会执行逻辑上的压栈操作,而局部变量离开作用域之后,就会被自动释放,所谓的自动释放就是下一次的压栈过程中,会被无条件覆盖。对于堆而言,申请内存时会从维护的堆地址中分配空间,归还时内存合并到维护的地址空间中。
Go程序运行时把 “栈” 全部消耗了,用于维护各个组件之间的协调,调度器、垃圾回收、系统调用等。
Go语言所消耗的 “堆和栈” 其实只是Go运行时通过管理向操作系统申请的堆内存,构造的逻辑上的 “堆和栈”。
相较于C/C++只有1MB的栈而言,Go程序用于 “几乎” 无限的栈内存(1GB)。
Go语言运行时为了防止内存碎片化,会在适当的时候对整个栈进行深拷贝。也正是这个原因,指针的算数运算不再能奏效,无法确定运算前后指针所指向的地址的内容是否已经被Go运行时移动。