Golang之内存逃逸 #
内存分配 #
在Golang中,程序申请的一个对象,是在栈上分配,还是在堆上分配,这个是由编译器决定的。
- 在栈上分配:在程序中,每个函数块都会有自己的内存区域来存储局部变量、返回值等数据,这一块内存区域有特定的数据结构与存储方式,其大小在程序编译的时候就已经分配好了,且这块区域寻址快,开销少,这块内存区域就称为栈区。因为栈的大小在编译的时候就已经确定了,所以当栈存储的数据过大时,会发生“栈溢出”。
- 在堆上分配:在程序中,全局变量,内存占用大的局部变量,发生了内存逃逸的局部变量就分配在堆上,这一块内存区域没有特定的结构,也没有固定的大小,可以根据需要进行调整。当一个变量分配在堆上的时候,开销会比较大,对Golang这种自带GC的语言来说,会增加GC压力,同时也会带来内存碎片。
内存逃逸 #
在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
Golang中的内存逃逸 #
在Golang中可以使用go run -gcflags “-m -l” (-m打印逃逸分析信息,-l禁止内联编译)来分析内存逃逸。使用如下代码来分析内存逃逸:
func foo() *int {
i := 10
return &i
}
func main() {
a := *foo()
_ = a
}执行结果如下:
go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:4:5: moved to heap: iGolang中内存逃逸的情况分为如下几种:
必然会逃逸:
- 在函数内返回局部指针变量(被外部引用时生命周期大于当前函数栈)。
- 被已经逃逸的变量引用的指针,一定发生逃逸。
- slice初始化空间大小不确定(slice重新分配地址扩充时会往堆上分配)。
- slice、map、channel存储指针或者带指针的值,一定发生逃逸(无法知道goroutine什么时候调用这些值)。
可能会逃逸:
- 将指针作为参数传给别的函数,在这个函数对这个参数指针的处理中,如果发生上面三种情况,则发生逃逸,否则不会逃逸。
Golang中如何避免逃逸 #
如果一个变量发生了内存逃逸,那么他的生命周期就变得不可知,虽然Golang的GC机制会帮助程序员自动释放不再被引用的内存,但大量的GC仍然会影响程序的运行性能,所以要尽量减少程序的GC操作。避免内存逃逸的方法:
- 对于占用内存小且只读的数据,传值会有更好的性能。
- 预先设定好slice的长度,因为在编译期无法确定切片长度,slice只能在堆上分配。
- 对性能要求较高且访问频次较高的函数,谨慎使用interface调用方法。
- 如果小对象过多,对象创建与销毁频繁,导致GC压力大,可以考虑使用sync.Pool。