目錄
什么是棧
什么是堆
棧 vs 堆(核心區別)
GO編譯器的逃逸分析
什么是逃逸分析?
怎么看逃逸分析結果?
典型“會逃逸”的場景
閉包捕獲局部變量
返回或保存帶有“底層存儲”的容器
經由接口/反射/fmt 等導致裝箱或被長期保存
把指針/引用存入全局、堆對象或長生命周期結構
???????參數“內容”被函數保留(編譯器推不動)
GO中的“堆/棧”怎么落地
落地
是否上堆由逃逸分析決定
什么時候“應該”用誰?
什么是棧
棧是每個線程私有、按先進后出管理的調用臨時區。
什么是堆
堆是進程共享、由內存分配器/GC統一管理的動態內存區。
棧 vs 堆(核心區別)
-
歸屬:棧是“每個線程一個棧”;堆是“整個進程(多線程)共享一大片內存”
-
用途:棧放調用過程相關的數據(返回地址、保存寄存器、局部變量等);堆放動態創建、可跨函數/長期存在的數據
-
生命周期:棧隨函數返回自動回收(棧幀彈出);堆由程序(
free/delete
)或垃圾回收器回收 -
分配/釋放成本:棧是簡單的指針移動,極快;堆需要向分配器申請/釋放,相對慢
-
訪問局部性:棧一般連續,緩存友好;堆可能碎片化
-
常見錯誤:棧——遞歸太深/局部數組過大導致棧溢出;堆——內存泄漏/重用已釋放內存/碎片
-
大小:棧通常較小且固定/可增長(按語言/平臺而定);堆通常大很多(由 OS/運行時管理)
GO編譯器的逃逸分析
go語言編譯器會自動決定把一個變量放在棧還是放在堆,編譯器會做逃逸分析(escape analysis),當發現變量的作用域沒有跑出函數范圍,就可以在棧上,反之則必須分配在堆。 go語言聲稱這樣可以釋放程序員關于內存的使用限制,更多的讓程序員關注于程序功能邏輯本身。
什么是逃逸分析?
編譯器在編譯期判斷一個變量是否會在當前函數返回后仍被引用(“逃出”當前棧幀)。
-
不逃逸 → 盡量放在棧上,分配/回收極快;
-
逃逸 → 放到堆上,由運行時/GC 管理,帶來分配與 GC 成本。
怎么看逃逸分析結果?
在你的包目錄運行(推薦關掉內聯更好讀):
go build -gcflags='all=-m -l' ./...
# 或者對測試/基準:
go test -run=^$ -bench=. -gcflags='all=-m -l' ./...
常見輸出含義:
-
moved to heap: x
/escapes to heap
:變量x
上堆了 -
does not escape
:未逃逸(可棧上) -
leaking param
/leaking param content
:把參數或其內容泄露到了可能長壽命的位置(導致逃逸)
典型“會逃逸”的場景
返回局部變量的地址/引用
func f1() *int {x := 10return &x // x 逃逸:必須活到函數返回之后
}
閉包捕獲局部變量
func f2() func() {x := 0return func() { x++ } // x 被閉包捕獲 → 逃逸
}
返回或保存帶有“底層存儲”的容器
func f3(n int) []int {s := make([]int, n)return s // s 的底層數組需在返回后仍然存活 → 逃逸
}
經由接口/反射/fmt 等導致裝箱或被長期保存
func f4(b []byte) {_ = fmt.Sprintf("%x", b) // b 常見會逃逸(fmt 可變參、接口裝箱)
}
???????把指針/引用存入全局、堆對象或長生命周期結構
var g []*T
func f5(p *T) {g = append(g, p) // p 的“內容”被長期保存 → 逃逸
}
???????參數“內容”被函數保留(編譯器推不動)
func keepPtr(pp **int) { stash = *pp } // 比如存到包級變量
func caller() {x := 1p := &xkeepPtr(&p) // 報 "leaking param content: p"
}
GO中的“堆/棧”怎么落地
落地
-
語法上沒有顯式“棧/堆”關鍵字;逃逸分析決定變量放棧還是堆
-
一般規律:不逃逸的局部數據可在棧上;返回到函數外/被閉包捕獲等會逃逸到堆
-
make
(slice/map/channel)和new
只是創建方式,是否上堆由逃逸分析決定,堆內存由 GC 回收
是否上堆由逃逸分析決定
type T struct { buf [1024]byte }func f() *T {t := T{} // 語義上是局部變量;若返回其地址 => 逃逸到堆(由編譯器決定)return &t
}func g() {t := T{} // 不逃逸的話,可能在棧上分配,函數返回就回收_ = t
}
什么時候“應該”用誰?
-
短生命周期、只在當前調用鏈內使用:棧(語言通常自動用棧)
-
需要跨函數/跨協程/長期緩存:堆(動態分配)
-
在擁有 GC 的語言(Go/Java/Python)中,寫法更關注語義,由編譯器/運行時決定最終放棧還是堆;只需要留意可能引發逃逸的用法和不必要的大對象分配。