什么是內(nèi)存逃逸楼雹?
在程序中详幽,每個函數(shù)塊都會有自己的內(nèi)存區(qū)域用來存自己的局部變量(內(nèi)存占用少)、返回地址恢共、返回值之類的數(shù)據(jù)战秋,這一塊內(nèi)存區(qū)域有特定的結(jié)構(gòu)和尋址方式,尋址起來十分迅速讨韭,開銷很少脂信。這一塊內(nèi)存地址稱為棧。棧是線程級別的透硝,大小在創(chuàng)建的時候已經(jī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ù)需要進行調(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本身卻是逃逸到堆上
}
- 切片擴容后長度太大滚躯,導(dǎo)致棾牛空間不足,逃逸到堆上掸掏。
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)用徽鼎,謹慎使用 interface 調(diào)用方法。
參考
簡單聊聊內(nèi)存逃逸 | 劍指offer - golang
Golang內(nèi)存逃逸是什么?怎么避免內(nèi)存逃逸否淤?