簡單聊聊內存逃逸 | 劍指offer - golang


問題

簡單講講golang的內存逃逸嗎?

解析

什么是內存逃逸

在程序中,每個函數塊都會有自己的內存區域用來存自己的局部變量(內存占用少)、返回地址、返回值之類的數據,這一塊內存區域有特定的結構和尋址方式,尋址起來十分迅速,開銷很少。這一塊內存地址稱為棧。棧是線程級別的,大小在創建的時候已經確定,當變量太大的時候,會"逃逸"到堆上,這種現象稱為內存逃逸。簡單來說,局部變量通過堆分配和回收,就叫內存逃逸。

內存逃逸的危害

堆是一塊沒有特定結構,也沒有固定大小的內存區域,可以根據需要進行調整。全局變量,內存占用較大的局部變量,函數調用結束后不能立刻回收的局部變量都會存在堆里面。變量在堆上的分配和回收都比在棧上開銷大的多。對於 go 這種帶 GC 的語言來說,會增加 gc 壓力,同時也容易造成內存碎片。

如何分析程序是否發生內存逃逸

build時添加-gcflags=-m 選項可分析內存逃逸情況,比如輸出./main.go:3:6: moved to heap: x 表示局部變量x逃逸到了堆上。

內存逃逸發生時機

  1. 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
}
  1. 局部變量在函數調用結束后還被其他地方使用,比如函數返回局部變量指針或閉包中引用包外的值。因為變量的生命周期可能會超過函數周期,因此只能放入堆中。
package main

func Foo () func (){
	x := 5			// x發生逃逸,因為在Foo調用完成后,被閉包函數用到,還不能回收,只能放到堆上存放
	return func () {
		x += 1
	}
}
func main() {
	inner := Foo()
	inner()
}
  1. 在 slice 或 map 中存儲指針。比如 []*string,其后面的數組可能是在棧上分配的,但其引用的值還是在堆上。
package main
func main() {
	var x int
	x = 10
	var ls []*int
	ls = append(ls, &x)		// x發生逃逸,ls存儲的是指針,所以ls底層的數組雖然在棧存儲,但x本身卻是逃逸到堆上
}
  1. 切片擴容后長度太大,導致棧空間不足,逃逸到堆上。
package main

func main() {
    s := make([]int, 10000, 10000)
    for index, _ := range s {
        s[index] = index
    }
}
  1. 在 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」,專注大白話分享實用技術。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM