輕鬆理解 Go 中的內存逃逸問題

內存逃逸是什麼

  1. 在程序中,每個函數塊都會有自己的內存區域用來存自己的局部變量(內存佔用少)、返回地址、返回值之類的數據,這一塊內存區域有特定的結構和尋址方式,尋址起來十分迅速,開銷很少。這一塊內存地址稱爲棧。

  2. 棧是線程級別的,大小在創建的時候已經確定,當變量太大的時候,會 "逃逸" 到堆上,這種現象稱爲內存逃逸。

  3. 簡單來說,局部變量通過堆分配和回收,就叫內存逃逸。

內存逃逸危害

  1. 堆是一塊沒有特定結構,也沒有固定大小的內存區域,可以根據需要進行調整。

  2. 全局變量,內存佔用較大的局部變量,函數調用結束後不能立刻回收的局部變量都會存在堆裏面。

  3. 變量在堆上的分配和回收都比在棧上開銷大的多。

  4. 對於 go 這種帶 GC 的語言來說,會增加 gc 壓力,同時也容易造成內存碎片。

內存逃逸現象

  1. 向 channel 發送指針數據。因爲在編譯時,不知道 channel 中的數據會被哪個 goroutine 接收,因此編譯器沒法知道變量什麼時候纔會被釋放,因此只能放入堆中。

  2. 局部變量在函數調用結束後還被其他地方使用,比如函數返回局部變量指針或閉包中引用包外的值。因爲變量的生命週期可能會超過函數週期,因此只能放入堆中。

  3. 在 slice 或 map 中存儲指針。比如 []*string,其後面的數組可能是在棧上分配的,但其引用的值還是在堆上。

  4. 切片擴容後長度太大,導致棧空間不足,逃逸到堆上。

  5. 在 interface 類型上調用方法。在 interface 類型上調用方法時會把 interface 變量使用堆分配, 因爲方法的真正實現只能在運行時知道。

逃逸分析原則

  1. 編譯階段無法確定的參數,會逃逸到堆上;

  2. 變量在函數外部存在引用,會逃逸到堆上;不存在引用,則會繼續在棧上;

  3. 變量佔用內存較大時,會逃逸到堆上;

內存逃逸解決

  1. 對於小型的數據,使用傳值而不是傳指針,避免內存逃逸。

  2. 避免使用長度不固定的 slice 切片,在編譯期無法確定切片長度,只能將切片使用堆分配。

  3. interface 調用方法會發生內存逃逸,在熱點代碼片段,謹慎使用。避免內存逃逸需要遵循如下兩個原則:

  4. 指向棧對象上的指針不能被存儲到堆中。

  5. 指向棧對象上的指針不能超過該棧對象的聲明週期。

具體案例

參數爲 interface 類型會逃逸

下面通過舉例,來進一步論證逃逸分析的原則,加深一下理解

我們可以使用這個命令go build -gcflags '-m -m -l' go文件名,來查看逃逸分析的結果。

func main() {
  num := 1
  fmt.Println(num)
}

原因分析:

func Println(a ...interface{}) (n int, err error),這個函數的入參是interface類型,編譯階段無法確定其具體的參數類型,所以內存分配到堆上

變量在函數外部有引用會逃逸

func main() {
  _ = test()
}

func test() *int {
  num := 10
  return &num
}

原因分析:

變量 num 在函數外部存在引用,函數退出時棧中的內存(棧幀)已經釋放,但引用已經被返回,如果通過引用地址取值,在棧中是取不到值的,所以 Go 爲了避免這個情況,會將內存分配到堆上。

變量佔用內存較大時會逃逸

func main() {
  //不會逃逸
  s1 := make([]int, 10, 10)
  for i := 0; i < 10; i++ {
    s1[i] = i
  }

  //會逃逸
  s2 := make([]int, 10000, 10000)
  for i := 0; i < 10000; i++ {
    s2[i] = i
  }
}

原因分析:

切片容量過大時,會產生逃逸,內存分配到堆上;容量小時,不會逃逸,內存分配依賴在棧上。

變量大小不確定時會逃逸

func main() {
  num := 10
  s := make([]int, num, num) 
  for i := 0; i < num; i++ {
    s[i] = i
  }
}

原因分析:

切片的長度和容量,雖然通過聲明的變量 num 來指定了,但在編譯階段是未知的,並不確定 num 的具體值,所以會逃逸,將內存分配到堆上。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vIPn2QgCkdlSMRSMRikbDA