問題
簡單講講golang的內存逃逸嗎?
解析
什么是內存逃逸
在程序中,每個函數塊都會有自己的內存區域用來存自己的局部變量(內存占用少)、返回地址、返回值之類的數據,這一塊內存區域有特定的結構和尋址方式,尋址起來十分迅速,開銷很少。這一塊內存地址稱為棧。棧是線程級別的,大小在創建的時候已經確定,當變量太大的時候,會"逃逸"到堆上,這種現象稱為內存逃逸。簡單來說,局部變量通過堆分配和回收,就叫內存逃逸。
內存逃逸的危害
堆是一塊沒有特定結構,也沒有固定大小的內存區域,可以根據需要進行調整。全局變量,內存占用較大的局部變量,函數調用結束后不能立刻回收的局部變量都會存在堆里面。變量在堆上的分配和回收都比在棧上開銷大的多。對於 go 這種帶 GC 的語言來說,會增加 gc 壓力,同時也容易造成內存碎片。
如何分析程序是否發生內存逃逸
build時添加-gcflags=-m
選項可分析內存逃逸情況,比如輸出./main.go:3:6: moved to heap: x
表示局部變量x逃逸到了堆上。
內存逃逸發生時機
- 向
channel
發送指針數據。因為在編譯時,不知道channel中的數據會被哪個 goroutine 接收,因此編譯器沒法知道變量什么時候才會被釋放,因此只能放入堆中。
package main
func main() {
ch := make(chan int, 1)
x := 5
ch <- x // x不發生逃逸,因為只是復制的值
ch1 := make(chan *int, 1)
y := 5
py := &y
ch1 <- py // y逃逸,因為y地址傳入了chan中,編譯時無法確定什么時候會被接收,所以也無法在函數返回后回收y
}
- 局部變量在函數調用結束后還被其他地方使用,比如函數返回局部變量指針或閉包中引用包外的值。因為變量的生命周期可能會超過函數周期,因此只能放入堆中。
package main
func Foo () func (){
x := 5 // x發生逃逸,因為在Foo調用完成后,被閉包函數用到,還不能回收,只能放到堆上存放
return func () {
x += 1
}
}
func main() {
inner := Foo()
inner()
}
- 在 slice 或 map 中存儲指針。比如 []*string,其后面的數組可能是在棧上分配的,但其引用的值還是在堆上。
package main
func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x) // x發生逃逸,ls存儲的是指針,所以ls底層的數組雖然在棧存儲,但x本身卻是逃逸到堆上
}
- 切片擴容后長度太大,導致棧空間不足,逃逸到堆上。
package main
func main() {
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}
- 在 interface 類型上調用方法。 在 interface 類型上調用方法時會把interface變量使用堆分配, 因為方法的真正實現只能在運行時知道。
package main
type foo interface {
fooFunc()
}
type foo1 struct{}
func (f1 foo1) fooFunc() {}
func main() {
var f foo
f = foo1{}
f.fooFunc() // 調用方法時,f發生逃逸,因為方法是動態分配的
}
避免內存逃逸的辦法
- 對於小型的數據,使用傳值而不是傳指針,避免內存逃逸。
- 避免使用長度不固定的slice切片,在編譯期無法確定切片長度,只能將切片使用堆分配。
- interface調用方法會發生內存逃逸,在熱點代碼片段,謹慎使用。
寫在最后
喜歡本文的朋友,歡迎關注公眾號「會玩 code」,專注大白話分享實用技術。