每天一個知識點:Golang 內(nèi)存逃逸

什么是內(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)存逃逸?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么要盡量避免內(nèi)存逃逸? 因為如果變量的內(nèi)存發(fā)生逃逸,它的生命周期就是不可知的,其會被分配到堆上,而堆上分配內(nèi)存...
    dongzd閱讀 1,389評論 0 1
  • 參考資源一參考資源二參考資源三 對于手動管理內(nèi)存的語言,比如 C/C++,調(diào)用著名的malloc和new函數(shù)可以在...
    簾外五更風(fēng)閱讀 1,202評論 0 0
  • 問題 知道golang的內(nèi)存逃逸嗎?什么情況下會發(fā)生內(nèi)存逃逸? 怎么答 golang程序變量會攜帶有一組校驗數(shù)據(jù),...
    9號閱讀 722評論 0 1
  • 一、類加載機(jī)制 類加載就是虛擬機(jī)把Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗,解析和初始化,形成可以虛擬機(jī)直接使用...
    xiaoqunzi233閱讀 363評論 0 0
  • 一、我們說內(nèi)存逃逸時在說什么 問,內(nèi)存逃逸是干什么的答,內(nèi)存逃逸分析是編譯器在編譯優(yōu)化時,用來決定變量應(yīng)該分配在堆...
    銀角代王閱讀 1,578評論 0 4

友情鏈接更多精彩內(nèi)容