Golang內存管理之內存逃逸分析
0. 簡介
前面我們針對Go中堆和棧的內存都做了一些分析,現(xiàn)在我們來分析一下Go的內存逃逸。
學習過C語言的都知道,在C棧區(qū)域會存放函數(shù)的參數(shù)、局部變量等,而這些局部變量的地址是不能返回的,除非是局部靜態(tài)變量地址,字符串常量地址或者動態(tài)分配的地址,因為程序調用完函數(shù)后,局部變量會隨著此函數(shù)的棧幀一起被釋放。而對于程序員主動申請的內存則存儲在堆上,需要使用malloc等函數(shù)進行申請,同時也需要使用free等函數(shù)釋放,由程序員進行管理,而申請內存后如果沒有釋放,就有可能造成內存泄漏。
但是在Go中,程序員根本無需感知數(shù)據(jù)是在棧(Go棧)上,還是在堆上,因為編譯器會幫你承擔這一切,將內存分配到棧或者堆上。在編譯器優(yōu)化中,逃逸分析是用來決定指針動態(tài)作用域的方法。Go語言的編譯器使用逃逸分析決定哪些變量應該分配在棧上,哪些變量應該分配在堆上,包括使用new、make和字面量等方式隱式分配的內存,Go語言逃逸分析遵循以下兩個不變性:
- 指向棧對象的指針不能存在于堆中;
- 指向棧對象的指針不能在棧對象回收后存活;
逃逸分析是在編譯階段進行的,可以通過go build -gcflags="-m -m -l"命令查到逃逸分析的結果,最多可以提供4個-m, m 越多則表示分析的程度越詳細,一般情況下我們可以采用兩個-m分析。使用-l禁用掉內聯(lián)優(yōu)化,只關注逃逸優(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)
}逃逸分析結果如下:
$ 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
分析結果很明顯,函數(shù)返回的局部變量是一個指針變量,當函數(shù)Add執(zhí)行結束后,對應的棧幀就會被銷毀,引用返回到函數(shù)之外,如果在外部解引用這個地址,就會導致程序訪問非法內存,所以編譯器會經(jīng)過逃逸分析后在堆上分配內存。
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ā)現(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
再看運行結果,發(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.
翻譯過來大意是:當逃逸分析輸出“b escapes to heap”時,意思是指存儲在b中的值逃逸到堆上了,即任何被b引用的對象必須分配在堆上,而b自身則不需要;如果b自身也逃逸到堆上,那么逃逸分析會輸出“&b escapes to heap”。
由于字符串本身是存儲在只讀存儲區(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這個對象的地址
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
打印結果:
$ go run main.go
&i 0xc00009af20
&b 0xc00009af58
b 0xc00009af28
可以看到,以上分析無逃逸,且&i b &b地址連續(xù),可以明顯看到都在棧中。
切片底層數(shù)組逃逸
我們新增一個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的底層數(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,然后查看打?。?/p>
$ go run main.go
&i 0xc000106f38
&b 0xc000106f58
b 0xc000120030
[1 2 3 4 5]
可以發(fā)現(xiàn),b的底層數(shù)組發(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的底層數(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這個對象本身也發(fā)生了逃逸。
所以可以總結下來就是:
escapes to heap:表示這個對象里面的指針對象逃逸到堆中;moved to heap:表示對象本身逃逸到堆中,根據(jù)指向棧對象的指針不能存在于堆中這一準則,該對象里面的指針對象特必然逃逸到堆中。
1.3 申請??臻g過大
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ā)現(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,則會發(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,超過這個數(shù)值就會發(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
因為函數(shù)也是一個指針類型,所以匿名函數(shù)當作返回值時也發(fā)生了逃逸,在匿名函數(shù)中使用外部變量i,這個變量i會一直存在直到a被銷毀,所以i變量逃逸到了堆上。
2. 總結
逃逸到堆上的內存可能會加大GC壓力,所以在一些簡單的場景下,我們可以避免內存逃逸,使得變量更多地分配在棧上,可以提升程序的性能。比如:
- 不要盲目地使用指針傳參,特別是參數(shù)對象很小時,雖然可以減小復制大小,但是可能會造成內存逃逸;
- 多根據(jù)代碼具體分析,根據(jù)逃逸分析結果做一些優(yōu)化,提高性能。
以上就是Golang內存管理之內存逃逸分析的詳細內容,更多關于Golang內存逃逸的資料請關注腳本之家其它相關文章!
相關文章
golang實現(xiàn)文件上傳并轉存數(shù)據(jù)庫功能
這篇文章主要為大家詳細介紹了golang實現(xiàn)文件上傳并轉存數(shù)據(jù)庫功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07

