什么是內(nèi)存逃逸?
在程序中,每個函數(shù)塊都會有自己的內(nèi)存區(qū)域用來存自己的局部變量(內(nèi)存占用少)、返回地址、返回值之類的數(shù)據(jù),這一塊內(nèi)存區(qū)域有特定的結(jié)構(gòu)和尋址方式,尋址起來十分迅速,開銷很少。這一塊內(nèi)存地址稱為棧。棧是線程級別的,大小在創(chuàng)建的時候已經(jīng)確定,當(dāng)變量太大的時候,會"逃逸"到堆上,這種現(xiàn)象稱為內(nèi)存逃逸。簡單來說,局部變量通過堆分配和回收,就叫內(nèi)存逃逸。
逃逸是如何產(chǎn)生的?
如果一個函數(shù)返回對一個變量的引用,那么它就會發(fā)生逃逸。即任何時候,一個值被分享到函數(shù)棧范圍之外,它都會在堆上被重新分配。在這里有一個例外,就是如果編譯器可以證明在函數(shù)返回后不會再被引用的,那么就會分配到棧上,這個證明的過程叫做逃逸分析。
總結(jié):
- 如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;
- 如果函數(shù)外部存在引用,則必定放到堆中;
內(nèi)存逃逸的危害
堆是一塊沒有特定結(jié)構(gòu),也沒有固定大小的內(nèi)存區(qū)域,可以根據(jù)需要進(jìn)行調(diào)整。全局變量,內(nèi)存占用較大的局部變量,函數(shù)調(diào)用結(jié)束后不能立刻回收的局部變量都會存在堆里面。變量在堆上的分配和回收都比在棧上開銷大的多。對于 go 這種帶 GC 的語言來說,會增加 gc 壓力,同時也容易造成內(nèi)存碎片(采用分區(qū)式存儲管理的系統(tǒng),在儲存分配過程中產(chǎn)生的、不能供用戶作業(yè)使用的主存里的小分區(qū)稱成“內(nèi)存碎片”。內(nèi)存碎片分為內(nèi)部碎片和外部碎片)。
逃逸的例子
- 向channel發(fā)送指針數(shù)據(jù)。因為在編譯時,不知道channel中的數(shù)據(jù)會被哪個 goroutine 接收,因此編譯器沒法知道變量什么時候才會被釋放,因此只能放入堆中。
package main
func main() {
ch := make(chan int, 1)
x := 5
ch <- x // x 不發(fā)生逃逸,因為只是復(fù)制的值
ch1 := make(chan *int, 1)
y := 5
py := &y
ch1 <- py // y 逃逸,因為 y 地址傳入了chan中,編譯時無法確定什么時候會被接收,所以也無法在函數(shù)返回后回收 y
}
- 局部變量在函數(shù)調(diào)用結(jié)束后還被其他地方使用,比如函數(shù)返回局部變量指針或閉包中引用包外的值。因為變量的生命周期可能會超過函數(shù)周期,因此只能放入堆中。
func Foo () func (){
x := 5 // x發(fā)生逃逸,因為在Foo調(diào)用完成后,被閉包函數(shù)用到,還不能回收,只能放到堆上存放
return func () {
x += 1
}
}
func main() {
inner := Foo()
inner()
}
- 在 slice 或 map 中存儲指針。比如 []*string,其后面的數(shù)組可能是在棧上分配的,但其引用的值還是在堆上。
package main
func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x) // x發(fā)生逃逸,ls存儲的是指針,所以ls底層的數(shù)組雖然在棧存儲,但x本身卻是逃逸到堆上
}
- 切片擴(kuò)容后長度太大,導(dǎo)致??臻g不足,逃逸到堆上。
func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x) // x發(fā)生逃逸,ls存儲的是指針,所以ls底層的數(shù)組雖然在棧存儲,但x本身卻是逃逸到堆上
}
- 在 interface 類型上調(diào)用方法。 在 interface 類型上調(diào)用方法時會把interface變量使用堆分配, 因為方法的真正實現(xiàn)只能在運行時知道。
package main
type foo interface {
fooFunc()
}
type foo1 struct{}
func (f1 foo1) fooFunc() {}
func main() {
var f foo
f = foo1{}
f.fooFunc() // 調(diào)用方法時,f發(fā)生逃逸,因為方法是動態(tài)分配的
}
如何避免內(nèi)存逃逸?
- 對于小型的數(shù)據(jù),使用傳值而不是傳指針,避免內(nèi)存逃逸。
- 對于需要修改原對象值,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇傳指針。
- 對于只讀的占用內(nèi)存較小的結(jié)構(gòu)體,直接傳值能夠獲得更好的性能。
- 盡量避免使用長度不固定的 slice 切片,因為在編譯期無法確定切片長度,只能將切片使用堆分配。
- 對于性能要求比較高且訪問頻次比較高的函數(shù)調(diào)用,謹(jǐn)慎使用 interface 調(diào)用方法。
參考
簡單聊聊內(nèi)存逃逸 | 劍指offer - golang
Golang內(nèi)存逃逸是什么?怎么避免內(nèi)存逃逸?