浅谈Go内存分配和逃逸分析

Go会使用make和new来为一些对象分配内存。但这不以为着使用了这两个方法分配的内存一定存储在堆上。 Go的编译器会根据对象的使用情况,进行智能的分配。

对于堆栈,可以这么理解:
栈区——由编译器自动分配和释放,一般存放函数的参数值、局部变量的值等(速度较快);
堆区——由程序员分配及释放,若程序员不释放,程序结束后可能由OS回收(速度比较慢,而且容易产生内存碎片)

也就是说,对于不同的变量,分配到不同的内存,这样方便程序的管理
局部变量在某个地方用完后就再不会使用的变量,那么应该分配到栈中,等一个函数运行结束的时候,自动进行退栈,这样就释放了内存,就不用程序员来手工释放。并且由于栈是一块连续的内存,退栈的操作也快了很多。
对于全局变量或者指针的情况就复杂的多了。全局变量一般都分配在堆中,使用new生成的指针,不像C++那样直接分配到堆中,而是编译器会主动分析该指针对象会不会在以后被使用到,如果不会使用到,那就分配到栈中,如果被使用到就分配到堆中。

好了,我们来看一个综合一点的例子:

func foo() *int {
	a := 10
	b := 20
	println(a)
	return &b
}

func main(){
	temp := foo()
	println(temp)
}

看到上面的代码,对于写过C++的同学可能就会发现这么写会出错的。因为在C++中没有使用new分配内存,就会导致b分配在栈区,在函数foo执行完之后被释放。而go却不会,因为go使用了逃逸分析,一旦发现局部变量逃逸出去,就主动的将其分配到堆中了。
执行go tool compile -S main.go反汇编看一下,以下是汇编的部分源码:

......
0x0024 00036 (main.go:8)        PCDATA  $1, $0
0x0024 00036 (main.go:8)        LEAQ    type.int(SB), AX
0x002b 00043 (main.go:8)        PCDATA  $0, $0
0x002b 00043 (main.go:8)        MOVQ    AX, (SP)
0x002f 00047 (main.go:8)        CALL    runtime.newobject(SB)
0x0034 00052 (main.go:8)        PCDATA  $0, $1
0x0034 00052 (main.go:8)        MOVQ    8(SP), AX
0x0039 00057 (main.go:8)        PCDATA  $1, $1
0x0039 00057 (main.go:8)        MOVQ    AX, "".&b+16(SP)
......

特别声明一下,在我的main.go文件中,第八行的代码就是b := 20。由于源码太长,我就截取了第八行的汇编代码。上面汇编中比较特别的就是runtime.newobject(SB),这句话就意味着该对象在被分配到堆上面了。而对于a := 10,并没有对a执行runtime.newobject(SB)。(各位可以在自己的电脑执行验证一下)
这是因为经过编译器分析过后,判定b会逃逸到foo函数外面去,所以主动的将b分配到堆中。
逃逸分析:
为了更加明确变量是否发生逃逸,go提供了逃逸的分析工具。
我们对源码执行go build -gcflags "-m -m -l" main.go。结果如下:

# command-line-arguments
.\main.go:8:2: b escapes to heap:
.\main.go:8:2:   flow: ~r0 = &b:
.\main.go:8:2:     from &b (address-of) at .\main.go:10:9
.\main.go:8:2:     from return &b (return) at .\main.go:10:2
.\main.go:8:2: moved to heap: b

结果很明确的告诉我们b逃逸到堆中去了。而a却没有发生什么。

由于Go使用了gc的方法来管理内存,也就是定期的扫描堆区,发现不再使用的变量就释放内存。这就导致了,程序在运行的过程中需要分配一部分运算能力给gc,以便定期扫描程序。当堆中的数据很多的时候,扫描也就会变慢,导致程序整体性能下降。因此尽量不要让变量逃逸到堆中。

撩我?
搜索我的微信公众号:Kyda
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章