Golang內(nèi)存管理之內(nèi)存逃逸分析
0. 簡介
前面我們針對(duì)Go中堆和棧的內(nèi)存都做了一些分析,現(xiàn)在我們來分析一下Go的內(nèi)存逃逸。
學(xué)習(xí)過C語言的都知道,在C棧區(qū)域會(huì)存放函數(shù)的參數(shù)、局部變量等,而這些局部變量的地址是不能返回的,除非是局部靜態(tài)變量地址,字符串常量地址或者動(dòng)態(tài)分配的地址,因?yàn)槌绦蛘{(diào)用完函數(shù)后,局部變量會(huì)隨著此函數(shù)的棧幀一起被釋放。而對(duì)于程序員主動(dòng)申請(qǐng)的內(nèi)存則存儲(chǔ)在堆上,需要使用malloc等函數(shù)進(jìn)行申請(qǐng),同時(shí)也需要使用free等函數(shù)釋放,由程序員進(jìn)行管理,而申請(qǐng)內(nèi)存后如果沒有釋放,就有可能造成內(nèi)存泄漏。
但是在Go中,程序員根本無需感知數(shù)據(jù)是在棧(Go棧)上,還是在堆上,因?yàn)榫幾g器會(huì)幫你承擔(dān)這一切,將內(nèi)存分配到?;蛘叨焉?。在編譯器優(yōu)化中,逃逸分析是用來決定指針動(dòng)態(tài)作用域的方法。Go語言的編譯器使用逃逸分析決定哪些變量應(yīng)該分配在棧上,哪些變量應(yīng)該分配在堆上,包括使用new、make和字面量等方式隱式分配的內(nèi)存,Go語言逃逸分析遵循以下兩個(gè)不變性:
- 指向棧對(duì)象的指針不能存在于堆中;
- 指向棧對(duì)象的指針不能在棧對(duì)象回收后存活;
逃逸分析是在編譯階段進(jìn)行的,可以通過go build -gcflags="-m -m -l"命令查到逃逸分析的結(jié)果,最多可以提供4個(gè)-m, m 越多則表示分析的程度越詳細(xì),一般情況下我們可以采用兩個(gè)-m分析。使用-l禁用掉內(nèi)聯(lián)優(yōu)化,只關(guān)注逃逸優(yōu)化即可。
1. 幾種逃逸分析
1.1 函數(shù)返回局部變量指針
package main
func Add(x, y int) *int {
res := 0
res = x + y
return &res
}
func main() {
Add(1, 2)
}逃逸分析結(jié)果如下:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:4:2: res escapes to heap:
./main.go:4:2: flow: ~r2 = &res:
./main.go:4:2: from &res (address-of) at ./main.go:6:9
./main.go:4:2: from return &res (return) at ./main.go:6:2
./main.go:4:2: moved to heap: res
note: module requires Go 1.18
分析結(jié)果很明顯,函數(shù)返回的局部變量是一個(gè)指針變量,當(dāng)函數(shù)Add執(zhí)行結(jié)束后,對(duì)應(yīng)的棧幀就會(huì)被銷毀,引用返回到函數(shù)之外,如果在外部解引用這個(gè)地址,就會(huì)導(dǎo)致程序訪問非法內(nèi)存,所以編譯器會(huì)經(jīng)過逃逸分析后在堆上分配內(nèi)存。
1.2 interface(any)類型逃逸
package main
import (
"fmt"
)
func main() {
str := "hello world"
fmt.Printf("%v\n", str)
}逃逸分析如下:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:9:13: str escapes to heap:
./main.go:9:13: flow: {storage for ... argument} = &{storage for str}:
./main.go:9:13: from str (spill) at ./main.go:9:13
./main.go:9:13: from ... argument (slice-literal-element) at ./main.go:9:12
./main.go:9:13: flow: {heap} = {storage for ... argument}:
./main.go:9:13: from ... argument (spill) at ./main.go:9:12
./main.go:9:13: from fmt.Printf("%v\n", ... argument...) (call parameter) at ./main.go:9:12
./main.go:9:12: ... argument does not escape
./main.go:9:13: str escapes to heap
通過這個(gè)分析你可能會(huì)認(rèn)為str escapes to heap表示這個(gè)str逃逸到了堆,但是卻沒有上一節(jié)中返回值中明確寫上moved to heap: res,那實(shí)際上str是否真的逃逸到了堆上呢?
escapes to heap vs moved to heap
我們可以寫如下代碼試試:
package main
import "fmt"
func main() {
str := "hello world"
str1 := "nihao!"
fmt.Printf("%s\n", str)
println(&str)
println(&str1)
}其逃逸分析和上面的沒有區(qū)別:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:8:13: str escapes to heap:
./main.go:8:13: flow: {storage for ... argument} = &{storage for str}:
./main.go:8:13: from str (spill) at ./main.go:8:13
./main.go:8:13: from ... argument (slice-literal-element) at ./main.go:8:12
./main.go:8:13: flow: {heap} = {storage for ... argument}:
./main.go:8:13: from ... argument (spill) at ./main.go:8:12
./main.go:8:13: from fmt.Printf("%s\n", ... argument...) (call parameter) at ./main.go:8:12
./main.go:8:12: ... argument does not escape
./main.go:8:13: str escapes to heap
note: module requires Go 1.18
但是,str1和str二者的地址卻是明顯相鄰的,那是怎么回事呢?
$ go run main.go
hello world
0xc00009af50
0xc00009af40
如果我們將上述代碼的第8行fmt.Printf("%s\n", str)改為fmt.Printf("%p\n", &str),則逃逸分析如下,發(fā)現(xiàn)多了一行moved to heap: str:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:6:2: str escapes to heap:
./main.go:6:2: flow: {storage for ... argument} = &str:
./main.go:6:2: from &str (address-of) at ./main.go:8:21
./main.go:6:2: from &str (interface-converted) at ./main.go:8:21
./main.go:6:2: from ... argument (slice-literal-element) at ./main.go:8:12
./main.go:6:2: flow: {heap} = {storage for ... argument}:
./main.go:6:2: from ... argument (spill) at ./main.go:8:12
./main.go:6:2: from fmt.Printf("%p\n", ... argument...) (call parameter) at ./main.go:8:12
./main.go:6:2: moved to heap: str
./main.go:8:12: ... argument does not escape
note: module requires Go 1.18
再看運(yùn)行結(jié)果,發(fā)現(xiàn)看起來str的地址看起來像逃逸到了堆,畢竟和str1的地址明顯不同:
$ go run main.go
0xc00010a210
0xc00010a210
0xc000106f50
參考如下解釋:
When the escape analysis says "b escapes to heap", it means that the values in
bare written to the heap. So anything referenced bybmust be in the heap also.bitself need not be.
翻譯過來大意是:當(dāng)逃逸分析輸出“b escapes to heap”時(shí),意思是指存儲(chǔ)在b中的值逃逸到堆上了,即任何被b引用的對(duì)象必須分配在堆上,而b自身則不需要;如果b自身也逃逸到堆上,那么逃逸分析會(huì)輸出“&b escapes to heap”。
由于字符串本身是存儲(chǔ)在只讀存儲(chǔ)區(qū),我們使用切片更能表現(xiàn)以上的特性。
無逃逸
package main
import (
"reflect"
"unsafe"
)
func main() {
var i int
i = 10
println("&i", &i)
b := []int{1, 2, 3, 4, 5}
println("&b", &b) // b這個(gè)對(duì)象的地址
println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
}逃逸分析是:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:12:12: []int{...} does not escape
note: module requires Go 1.18
打印結(jié)果:
$ go run main.go
&i 0xc00009af20
&b 0xc00009af58
b 0xc00009af28
可以看到,以上分析無逃逸,且&i b &b地址連續(xù),可以明顯看到都在棧中。
切片底層數(shù)組逃逸
我們新增一個(gè)fmt包的打?。?/p>
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var i int
i = 10
println("&i", &i)
b := []int{1, 2, 3, 4, 5}
println("&b", &b) // b這個(gè)對(duì)象的地址
println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
fmt.Println(b) // 多加了這行
}逃逸分析如下:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:16:13: b escapes to heap:
./main.go:16:13: flow: {storage for ... argument} = &{storage for b}:
./main.go:16:13: from b (spill) at ./main.go:16:13
./main.go:16:13: from ... argument (slice-literal-element) at ./main.go:16:13
./main.go:16:13: flow: {heap} = {storage for ... argument}:
./main.go:16:13: from ... argument (spill) at ./main.go:16:13
./main.go:16:13: from fmt.Println(... argument...) (call parameter) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap:
./main.go:13:12: flow: b = &{storage for []int{...}}:
./main.go:13:12: from []int{...} (spill) at ./main.go:13:12
./main.go:13:12: from b := []int{...} (assign) at ./main.go:13:4
./main.go:13:12: flow: {storage for b} = b:
./main.go:13:12: from b (interface-converted) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap
./main.go:16:13: ... argument does not escape
./main.go:16:13: b escapes to heap
note: module requires Go 1.18
可以發(fā)現(xiàn),出現(xiàn)了b escapes to heap,然后查看打印:
$ go run main.go
&i 0xc000106f38
&b 0xc000106f58
b 0xc000120030
[1 2 3 4 5]
可以發(fā)現(xiàn),b的底層數(shù)組發(fā)生了逃逸,但是b本身還是在棧中。
切片對(duì)象同樣發(fā)生逃逸
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var i int
i = 10
println("&i", &i)
b := []int{1, 2, 3, 4, 5}
println("&b", &b) // b這個(gè)對(duì)象的地址
println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
fmt.Println(&b) // 修改這行
}如上,將fmt.Println(b)改為fmt.Println(&b),逃逸分析如下:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:13:2: b escapes to heap:
./main.go:13:2: flow: {storage for ... argument} = &b:
./main.go:13:2: from &b (address-of) at ./main.go:16:14
./main.go:13:2: from &b (interface-converted) at ./main.go:16:14
./main.go:13:2: from ... argument (slice-literal-element) at ./main.go:16:13
./main.go:13:2: flow: {heap} = {storage for ... argument}:
./main.go:13:2: from ... argument (spill) at ./main.go:16:13
./main.go:13:2: from fmt.Println(... argument...) (call parameter) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap:
./main.go:13:12: flow: b = &{storage for []int{...}}:
./main.go:13:12: from []int{...} (spill) at ./main.go:13:12
./main.go:13:12: from b := []int{...} (assign) at ./main.go:13:4
./main.go:13:2: moved to heap: b
./main.go:13:12: []int{...} escapes to heap
./main.go:16:13: ... argument does not escape
note: module requires Go 1.18
發(fā)現(xiàn)多了moved to heap: b這行,然后看地址打?。?/p>
$ go run main.go
&i 0xc00006af48
&b 0xc00000c030
b 0xc00001a150
&[1 2 3 4 5]
發(fā)現(xiàn)不僅底層數(shù)組發(fā)生了逃逸,連b這個(gè)對(duì)象本身也發(fā)生了逃逸。
所以可以總結(jié)下來就是:
escapes to heap:表示這個(gè)對(duì)象里面的指針對(duì)象逃逸到堆中;moved to heap:表示對(duì)象本身逃逸到堆中,根據(jù)指向棧對(duì)象的指針不能存在于堆中這一準(zhǔn)則,該對(duì)象里面的指針對(duì)象特必然逃逸到堆中。
1.3 申請(qǐng)??臻g過大
package main
import (
"reflect"
"unsafe"
)
func main() {
var i int
i = 10
println("&i", &i)
b := make([]int, 0)
println("&b", &b) // b這個(gè)對(duì)象的地址
println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data))
b1 := make([]byte, 65536)
println("&b1", &b1) // b1這個(gè)對(duì)象的地址
println("b1", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b1)).Data))
var a [1024*1024*10]byte
_ = a
}可以發(fā)現(xiàn)逃逸分析顯示沒有發(fā)生逃逸:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:13:11: make([]int, 0) does not escape
./main.go:17:12: make([]byte, 65536) does not escape
note: module requires Go 1.18
如果將切片和數(shù)組的長度都增加1,則會(huì)發(fā)生逃逸。
b1 := make([]byte, 65537) var a [1024*1024*10 + 1]byte
逃逸分析:
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:21:6: a escapes to heap:
./main.go:21:6: flow: {heap} = &a:
./main.go:21:6: from a (too large for stack) at ./main.go:21:6
./main.go:17:12: make([]byte, 65537) escapes to heap:
./main.go:17:12: flow: {heap} = &{storage for make([]byte, 65537)}:
./main.go:17:12: from make([]byte, 65537) (too large for stack) at ./main.go:17:12
./main.go:21:6: moved to heap: a
./main.go:13:11: make([]int, 0) does not escape
./main.go:17:12: make([]byte, 65537) escapes to heap
note: module requires Go 1.18
可以發(fā)現(xiàn)切片類型的逃逸閾值是65536 = 64KB,數(shù)組類型的逃逸閾值是1024*1024*10 = 10MB,超過這個(gè)數(shù)值就會(huì)發(fā)生逃逸。
1.4 閉包逃逸
package main
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
a := intSeq()
println(a())
println(a())
println(a())
println(a())
println(a())
println(a())
}逃逸分析如下,可以發(fā)現(xiàn)閉包中的局部變量i發(fā)生了逃逸。
$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:4:2: intSeq capturing by ref: i (addr=false assign=true width=8)
./main.go:5:9: func literal escapes to heap:
./main.go:5:9: flow: ~r0 = &{storage for func literal}:
./main.go:5:9: from func literal (spill) at ./main.go:5:9
./main.go:5:9: from return func literal (return) at ./main.go:5:2
./main.go:4:2: i escapes to heap:
./main.go:4:2: flow: {storage for func literal} = &i:
./main.go:4:2: from i (captured by a closure) at ./main.go:6:3
./main.go:4:2: from i (reference) at ./main.go:6:3
./main.go:4:2: moved to heap: i
./main.go:5:9: func literal escapes to heap
note: module requires Go 1.18
因?yàn)楹瘮?shù)也是一個(gè)指針類型,所以匿名函數(shù)當(dāng)作返回值時(shí)也發(fā)生了逃逸,在匿名函數(shù)中使用外部變量i,這個(gè)變量i會(huì)一直存在直到a被銷毀,所以i變量逃逸到了堆上。
2. 總結(jié)
逃逸到堆上的內(nèi)存可能會(huì)加大GC壓力,所以在一些簡單的場(chǎng)景下,我們可以避免內(nèi)存逃逸,使得變量更多地分配在棧上,可以提升程序的性能。比如:
- 不要盲目地使用指針傳參,特別是參數(shù)對(duì)象很小時(shí),雖然可以減小復(fù)制大小,但是可能會(huì)造成內(nèi)存逃逸;
- 多根據(jù)代碼具體分析,根據(jù)逃逸分析結(jié)果做一些優(yōu)化,提高性能。
以上就是Golang內(nèi)存管理之內(nèi)存逃逸分析的詳細(xì)內(nèi)容,更多關(guān)于Golang內(nèi)存逃逸的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫功能
這篇文章主要為大家詳細(xì)介紹了golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
Go微服務(wù)網(wǎng)關(guān)的實(shí)現(xiàn)
本文主要介紹了Go微服務(wù)網(wǎng)關(guān)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Golang時(shí)間及時(shí)間戳的獲取轉(zhuǎn)換超全面詳細(xì)講解
說實(shí)話,golang的時(shí)間轉(zhuǎn)化還是很麻煩的,最起碼比php麻煩很多,下面這篇文章主要給大家介紹了關(guān)于golang時(shí)間/時(shí)間戳的獲取與轉(zhuǎn)換的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
golang time包做時(shí)間轉(zhuǎn)換操作
這篇文章主要介紹了golang time包做時(shí)間轉(zhuǎn)換操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12
golang中單機(jī)鎖的具體實(shí)現(xiàn)詳解
這篇文章主要為大家詳細(xì)介紹了golang中單機(jī)鎖的具體實(shí)現(xiàn)的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03

