1.1 逃逸分析是什么
逃逸分析是指編譯器在執行靜態代碼分析后,對內存管理進行的優化和簡化。
在編譯原理中,分析指針動態范圍的方法被稱為逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,則稱這個指針發生了逃逸。逃逸分析決定一個變量是分配在堆上還是分配在棧上。
1.2 逃逸分析有什么作用
逃逸分析把變量合理地分配到它該去的地方,“找準自己的位置”。既是使用 new 函數申請到的內存,如果編譯器發現這塊內存在退出函數后就沒有使用了,那就分配到棧上,畢竟棧上的內存分配比堆上快很多;反之,既是表面上只是一個普通的變量,但是經過編譯器的逃逸分析后發現,在函數之外還有其他的地方在引用,那就分配到堆上。真正做到 “按需分配”。
如果變量都分配到堆上,堆不像棧可以自動清理。就會引起 Go 頻繁地進行垃圾回收,而垃圾回收會占用比較大的系統開銷。
堆和棧相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片;棧內存分配則非常快。棧分配內存只需通過 PUSH 指令,并且會被自動釋放;而堆分配首先需要去找到一塊大小合適的內存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量變少了,會減輕堆內存分配的開銷,同時也會減少垃圾回收(Garbage Collction,GC)的壓力,提高程序運行速度。
1.3 逃逸分析是怎么完成的
Go 語言逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用,那么這個變量就會發生逃逸。
編譯器會分析代碼的特征和代碼的生命周期,Go 中的變量只有在編譯器可以證明在函數返回后不再被引用,才分配到棧上,其他情況下都是直接分配到堆上。
Go 語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上。相反,編譯器通過分析代碼來決定將變量分配到何處。
對一個變量取地址,可能會被分配到堆上。但是編譯器進行逃逸分析后,如果考慮到在函數返回后,此變量不會被引用,那么還是可能分配到棧上。簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸:
如果變量在函數外部沒有被引用,則優先放到棧上。
如果變量在函數外部存在引用,則必定放在堆上。
針對第一條,放到堆上的情形:定義了一個很大的數組,需要申請的內存過大,超過了棧的存儲能力。
1.4 如何確定是否發生逃逸分析
Go 提供了相關的命令,可以查看變量是否發生了逃逸。例子如下:
package mainimport "fmt"func foo() *int {t := 3return &t
}func main() {x := foo()fmt.Println(*x)
}
foo 函數返回一個局部變量的指針,使用 main 函數里變量 x 接收它。執行如下命令:
go build -gcflags '-m-l' main.go
其中 -gcflags 參數用于啟用編譯器支持的額外標志。例如, -m 用于輸出編譯器的優化細節(包括使用逃逸分析這種優化),相反可以使用 -N 來關閉編譯器優化;而 -l 則用于禁用 foo 函數的內聯優化,防止逃逸被編譯器通過內聯優化徹底的抹除。得到如下輸出:
### command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape
foo 函數里的變量 t 逃逸了,和預想的一致,不解的是為什么 main 函數里的 x 也逃逸了?這是以為有些函數的參數為 interface 類型,比如 fmt.Println(a …interface{}) ,編譯期間很難確定其參數的具體類型,也會發生逃逸。