問題
簡單講講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」,專注大白話分享實用技術,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/280152.html
標籤:Go
