Go内存逃逸分析
Go内存逃逸分析、底层优化等…
1. 关于 堆和栈
栈可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。
1 |
|
类似于上面代码里面的temp变量,只是内函数内部申请的临时变量,并不会作为返回值返回,它就是被编译器申请到栈里面。
申请到栈内存好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
再来看看堆得情况之一如下代码:
1 |
|
而上面这段代码,申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到
堆 上面了。
申请到堆上面的内存才会引起垃圾回收,如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。
我们再看看如下几个例子:
1 |
|
像是 b 这种 即使是临时变量,申请过大也会在堆上面申请。对于 c 编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。
2. 逃逸分析(Escape analysis)
逃逸分析指的是由编译器决定内存分配的位置,不需要由程序员指定.
在函数中申请一个新的对象
如果申请到栈中,则函数执行结束可以自动将内存回收.
如果分配到堆,则函数执行结束就可以交给GC处理.
3. 逃逸场景(什么情况才分配到堆中)
3.1 指针逃逸
Go可以返回局部变量指针,这其实是一个典型的变量逃逸案例,示例代码如下:
1 |
|
虽然 在函数 StudentRegister() 内部 s 为局部变量,其值通过函数返回值返回,s 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。
终端运行命令查看逃逸分析日志:
1 |
|

可见在StudentRegister()函数中,也即代码第9行显示”escapes to heap”,代表该行内存分配发生了逃逸现象。
3.2 栈空间不足逃逸(空间开辟过大)
1 |
|
上面代码Slice()函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。 直接查看编译提示,如下:

所以只是1000的长度还不足以发生逃逸现象。然后就x10倍吧
1 |
|
分析如下:当切片长度扩大到10000时就会逃逸。实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。
3.3 动态类型逃逸(不确定长度大小)
很多函数参数为interface类型,比如fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也能产生逃逸。
如下代码所示:
1 |
|
逃逸分下如下:
1 |
|
又或者像前面提到的例子:
1 |
|
3.4 闭包引用对象逃逸
Fibonacci数列的函数:
1 |
|
1 |
|
输出如下:
1 |
|
逃逸如下:
1 |
|
Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。
逃逸分析的作用是什么呢?
逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配
,而没有发生逃逸的则有编译器在栈上分配)。同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
逃逸总结:
栈上分配内存比在堆中分配内存有更高的效率
栈上分配的内存不需要GC处理
堆上分配的内存使用完毕会交给GC处理
逃逸分析目的是决定内分配地址是栈还是堆
逃逸分析在编译阶段完成
提问:函数传递指针真的比传值效率高吗?
我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。
在官网 (golang.org) FAQ 上有一个关于变量分配的问题如下:
From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to
it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate
variables that are local to a function in that function’s stack frame.
However, if the compiler cannot prove that the variable is not referenced after the function returns, then the
compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local
variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the
heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the
function and can reside on the stack.
翻译如下:
如何得知变量是分配在栈(stack)上还是堆(heap)上?
准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。
知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。
然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。
当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。