Go逃逸分析
1. 什么是逃逸分析
在C/C++中,对内存的操作经常需要小心翼翼,比如下面这段代码就隐藏了一个陷阱: 函数内部定义的局部变量,其内存是在栈上分配的(静态内存分布),函数执行完毕后内存会被销毁。因此这段代码会直接崩溃。
1 | |
为了避免这个问题,需要对这段代码做一点改进. 通过new创建的变量位于堆上,不会随着函数执行完成销毁。
1 | |
但是这样依旧有一个问题,调用者依旧需要记得在适当的时候删除这个对象,不然就会造成内存泄露。
综上,C/C++等语言的内存分配有两个痛点:
- 需要时刻注意内存的分配位置,是在栈上还是堆上
- 堆上的内存需要手动释放
Go优雅的解决了这两个问题: 通过逃逸分析决定内存分配的位置; 通过垃圾回收自动释放堆上的内存。
在编译原理中,分析指针动态范围的方法被称之为逃逸分析。当一个对象的指针被多个方法或线程引用时,则称这个指针发生了逃逸,逃逸分析决定一个变量分配在堆上还是栈上。
2. 逃逸分析的作用
逃逸分析把变量合理的分配到它该去的地方。即使是new函数申请的内存,如果函数退出后就没有用了,那么就会将内存分配到栈上。反之,即使一个普通变量,发现函数退出之后依旧还有引用,那就分配到堆上。
如果变量都分配到堆上,堆上的内存分配速度较慢,并且因为堆无法像栈一样自动释放内存,就会引起频繁的垃圾回收,从而消耗较多的性能。
3. 逃逸分析的原则
编译器会分析代码的特征和生命周期,只有在编译器可以证明函数返回后不会再被引用的变量才会分配到栈上,其他情况下都是分配到堆。分配原则如下:
- 如果变量在函数外部没有引用,则
优先放到栈上 - 如果变量在函数外部存在引用,则
一定放到堆上
第一条原则中,为什么是优先,而不是一定呢? 加入我们申请了一个很大的数组,申请内存过大,超过了栈的存储能力,这时候就会放到堆上。
4. 如何确定发生逃逸
Go提供了相关命令,可以查看是否发生逃逸
1 | |
- -m 用于输出编译器的优化细节
- -I 关闭内联优化,避免逃逸被编译器的内联优化抹除
5. Go的堆栈与C/C++的区别
C/C++中提到的堆与栈本质上是操作系统级别的概念,在程序启动时,操作系统会自动维护一个程序消耗内存的地址空间,并从逻辑上划分为堆内存和栈内存。此时申请一个局部变量,会执行压栈,当离开作用域后自动释放(自动释放的本质是该位置可被下次压栈覆盖);对于堆而言,每次申请会将所需的地址从维护的堆内存地址空间中分配出去,归还时再合并到所维护的地址空间中
Go既然也运行在操作系统上,自然也拥有上述堆与栈的概念。但是传统意义上的栈被Go的运行时全部消耗了,用于维护各个组件间的协调,例如调度器、垃圾回收等。对于用户态的Go代码,所消耗的堆和栈,实际上都是Go运行时向操作系统申请的堆内存,构成逻辑上的堆和栈。因此Go程序的栈空间相对只有1M的C/C++而言大得多(1GB)