淺談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
在這裏插入圖片描述

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