无GC语言是怎么运作的
一般来说,类似C/C++语言通过 malloc
等方法分配的内存是在heap上的,但在Golang中却不是这样的,即便使用 new
,也不一定分配在heap上,这也是我们今天要关注的问题。
那么为什么Go会这样呢?
其实也很简单,因为Go是有runtime
的,实际分配在heap还是stack是由runtime
决定的。看到这里可能会很奇怪,为什么有runtime
,有GC我们还要关心分配在哪里呢?一切交给runtime
和GC去管理就好了呀!
是的,确实是这样,但是有效的使用堆内存,避免因系统GC造成的STW带来的性能损失对于某些系统来说也是比较重要的。因此,接下来将说明Go的runtime
如何决定变量分配在哪里的。
什么是逃逸分析?
在进入下面的内容前,先来解释下什么是逃逸分析。
其实很简单
逃逸分析就是由编译器确定内存在heap还是在stack,而不是程序员决定。
更进一步
如果在函数中申请一个新的对象:
- 如果分配在栈中,则函数执行结束可自动将内存回收;
- 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;
那怎么确定发生了逃逸呢?
如果本该分配在stack上的内存分配到了heap,则发生了逃逸。
逃逸的场景在Go中有多个,下面将详细介绍
逃逸场景
要有效的使用堆内存,就需要了解什么情况下发生逃逸。
场景一:指针逃逸
因为Go有指针机制,因此我们可以在函数结束时返回一个指针
package main
func main() {
_ = f(10, 20)
}
func f(x, y int) *int {
n := new(int)
*n = x * y
return n
}
这段代码发生了逃逸,我们用 gcflags "-m -l"
来查看,其中 -l
是阻止内联优化
.\main.go:8:10: new(int) escapes to heap
编译时显示第8行代码分配在heap上,也就是 n := new(int)
这段代码
场景二:栈空间不足逃逸
当我们分配的变量内存超过stack空间时,也会发生逃逸,如下代码
package main
func main() {
_ = make([]int, 1000, 1000)
}
编译时提示
.\main.go:4:10: main make([]int, 1000, 1000) does not escape
没有发生逃逸,也就意味着没有超过stack空间,我们加大内存分配
package main
func main() {
_ = make([]int, 1000, 10000)
}
将数量调整至 10000,再进行编译提示
.\main.go:4:10: make([]int, 1000, 10000) escapes to heap
发生了逃逸
场景三:动态分配逃逸
比如下面这段代码
package main
func main() {
l := 10
_ = make([]int, l)
}
编译时提示
.\main.go:5:10: make([]int, l) escapes to heap
发生逃逸的原因很简单,因为 l
变量可能被更改,所以编译器认为应该分配到 heap
场景四:闭包引用对象逃逸
Go语言支持闭包机制的,因此也会发生逃逸,如下代码
package main
func main() {
_ = f()
}
func f() func() int {
a, b := 0, 1
return func() int {
return a + b
}
}
编译时提示
.\main.go:9:9: func literal escapes to heap
.\main.go:9:9: func literal escapes to heap
本来a
和b
作为函数局部变量应该分配到stack中,但是由于f()
函数返回了一个闭包函数,因此编译器认为a
和b
应该分配到heap
场景五:其他函数栈使用了该内存
package main
import "fmt"
func main() {
a := new(int)
fmt.Println(a)
}
编译时提示
.\main.go:7:13: a escapes to heap
.\main.go:6:10: new(int) escapes to heap
.\main.go:7:13: main ... argument does not escape
总结
逃逸分析在编译阶段完成,逃逸分析目的是决定内存地址分配在stack
上还是heap
上。
通过了解逃逸分析,我们在编写Go代码时就可以知道内存究竟是在heap
上分配还是在stack
上分配,然后更精细的控制heap
上的内存分配,减轻GC压力。