Golang內存管理之內存逃逸分析
0. 簡介
前面我們針對Go中堆和棧的內存都做了一些分析,現在我們來分析一下Go的內存逃逸。
學習過C語言的都知道,在C棧區(qū)域會存放函數的參數、局部變量等,而這些局部變量的地址是不能返回的,除非是局部靜態(tài)變量地址,字符串常量地址或者動態(tài)分配的地址,因為程序調用完函數后,局部變量會隨著此函數的棧幀一起被釋放。而對于程序員主動申請的內存則存儲在堆上,需要使用malloc
等函數進行申請,同時也需要使用free
等函數釋放,由程序員進行管理,而申請內存后如果沒有釋放,就有可能造成內存泄漏。
但是在Go中,程序員根本無需感知數據是在棧(Go棧)上,還是在堆上,因為編譯器會幫你承擔這一切,將內存分配到?;蛘叨焉?。在編譯器優(yōu)化中,逃逸分析是用來決定指針動態(tài)作用域的方法。Go語言的編譯器使用逃逸分析決定哪些變量應該分配在棧上,哪些變量應該分配在堆上,包括使用new
、make
和字面量等方式隱式分配的內存,Go語言逃逸分析遵循以下兩個不變性:
- 指向棧對象的指針不能存在于堆中;
- 指向棧對象的指針不能在棧對象回收后存活;
逃逸分析是在編譯階段進行的,可以通過go build -gcflags="-m -m -l"
命令查到逃逸分析的結果,最多可以提供4個-m
, m 越多則表示分析的程度越詳細,一般情況下我們可以采用兩個-m
分析。使用-l
禁用掉內聯優(yōu)化,只關注逃逸優(yōu)化即可。
1. 幾種逃逸分析
1.1 函數返回局部變量指針
package main func Add(x, y int) *int { res := 0 res = x + y return &res } func main() { Add(1, 2) }
逃逸分析結果如下:
$ 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
分析結果很明顯,函數返回的局部變量是一個指針變量,當函數Add
執(zhí)行結束后,對應的棧幀就會被銷毀,引用返回到函數之外,如果在外部解引用這個地址,就會導致程序訪問非法內存,所以編譯器會經過逃逸分析后在堆上分配內存。
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
通過這個分析你可能會認為str escapes to heap
表示這個str
逃逸到了堆,但是卻沒有上一節(jié)中返回值中明確寫上moved to heap: res
,那實際上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ā)現多了一行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
再看運行結果,發(fā)現看起來str
的地址看起來像逃逸到了堆,畢竟和str1
的地址明顯不同:
$ go run main.go
0xc00010a210
0xc00010a210
0xc000106f50
參考如下解釋:
When the escape analysis says "b escapes to heap", it means that the values in
b
are written to the heap. So anything referenced byb
must be in the heap also.b
itself need not be.
翻譯過來大意是:當逃逸分析輸出“b escapes to heap”時,意思是指存儲在b中的值逃逸到堆上了,即任何被b引用的對象必須分配在堆上,而b自身則不需要;如果b自身也逃逸到堆上,那么逃逸分析會輸出“&b escapes to heap”。
由于字符串本身是存儲在只讀存儲區(qū),我們使用切片更能表現以上的特性。
無逃逸
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這個對象的地址 println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數組地址 }
逃逸分析是:
$ 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
打印結果:
$ go run main.go
&i 0xc00009af20
&b 0xc00009af58
b 0xc00009af28
可以看到,以上分析無逃逸,且&i b &b
地址連續(xù),可以明顯看到都在棧中。
切片底層數組逃逸
我們新增一個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這個對象的地址 println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數組地址 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ā)現,出現了b escapes to heap
,然后查看打?。?/p>
$ go run main.go
&i 0xc000106f38
&b 0xc000106f58
b 0xc000120030
[1 2 3 4 5]
可以發(fā)現,b
的底層數組發(fā)生了逃逸,但是b
本身還是在棧中。
切片對象同樣發(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這個對象的地址 println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數組地址 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ā)現多了moved to heap: b
這行,然后看地址打印:
$ go run main.go
&i 0xc00006af48
&b 0xc00000c030
b 0xc00001a150
&[1 2 3 4 5]
發(fā)現不僅底層數組發(fā)生了逃逸,連b
這個對象本身也發(fā)生了逃逸。
所以可以總結下來就是:
escapes to heap
:表示這個對象里面的指針對象逃逸到堆中;moved to heap
:表示對象本身逃逸到堆中,根據指向棧對象的指針不能存在于堆中這一準則,該對象里面的指針對象特必然逃逸到堆中。
1.3 申請棧空間過大
package main import ( "reflect" "unsafe" ) func main() { var i int i = 10 println("&i", &i) b := make([]int, 0) println("&b", &b) // b這個對象的地址 println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) b1 := make([]byte, 65536) println("&b1", &b1) // b1這個對象的地址 println("b1", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b1)).Data)) var a [1024*1024*10]byte _ = a }
可以發(fā)現逃逸分析顯示沒有發(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
如果將切片和數組的長度都增加1,則會發(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ā)現切片類型的逃逸閾值是65536 = 64KB
,數組類型的逃逸閾值是1024*1024*10 = 10MB
,超過這個數值就會發(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ā)現閉包中的局部變量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
因為函數也是一個指針類型,所以匿名函數當作返回值時也發(fā)生了逃逸,在匿名函數中使用外部變量i
,這個變量i
會一直存在直到a
被銷毀,所以i
變量逃逸到了堆上。
2. 總結
逃逸到堆上的內存可能會加大GC壓力,所以在一些簡單的場景下,我們可以避免內存逃逸,使得變量更多地分配在棧上,可以提升程序的性能。比如:
- 不要盲目地使用指針傳參,特別是參數對象很小時,雖然可以減小復制大小,但是可能會造成內存逃逸;
- 多根據代碼具體分析,根據逃逸分析結果做一些優(yōu)化,提高性能。
以上就是Golang內存管理之內存逃逸分析的詳細內容,更多關于Golang內存逃逸的資料請關注腳本之家其它相關文章!
相關文章
Golang unsafe.Sizeof函數代碼示例使用解析
這篇文章主要為大家介紹了Golang unsafe.Sizeof函數代碼示例使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12Golang運行報錯找不到包:package?xxx?is?not?in?GOROOT的解決過程
這篇文章主要給大家介紹了關于Golang運行報錯找不到包:package?xxx?is?not?in?GOROOT的解決過程,文中通過圖文介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2022-07-07golang?struct?json?tag的使用以及深入講解
這篇文章主要給大家介紹了關于golang?struct?json?tag的使用以及深入講解,文中通過實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2022-02-02