對(duì)于手動(dòng)管理內(nèi)存的語(yǔ)言,比如 C/C++,調(diào)用著名的malloc和new函數(shù)可以在堆上分配一塊內(nèi)存,這塊內(nèi)存的使用和銷毀的責(zé)任都在程序員。一不小心,就會(huì)發(fā)生內(nèi)存泄露,搞得膽戰(zhàn)心驚。
但是 Golang 并不是這樣,雖然 Golang 語(yǔ)言里面也有 new。Golang 編譯器決定變量應(yīng)該分配到什么地方時(shí)會(huì)進(jìn)行逃逸分析。使用new函數(shù)得到的內(nèi)存不一定就在堆上。堆和棧的區(qū)別對(duì)程序員“模糊化”了,當(dāng)然這一切都是Go編譯器在背后幫我們完成的。一個(gè)變量是在堆上分配,還是在棧上分配,是經(jīng)過(guò)編譯器的逃逸分析之后得出的結(jié)論。
一、 逃逸分析是什么
wiki定義
In compiler optimization, escape analysis is a method for determining the dynamic scope of pointers - where in the program a pointer can be accessed. It is related to pointer analysis and shape analysis.
When a variable (or an object) is allocated in a subroutine, a pointer to the variable can escape to other threads of execution, or to calling subroutines. If an implementation uses tail call optimization (usually required for functional languages), objects may also be seen as escaping to called subroutines. If a language supports first-class continuations (as do Scheme and Standard ML of New Jersey), portions of the call stack may also escape.
If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has "escaped". Pointers can also escape if they are stored in global variables or other data structures that, in turn, escape the current procedure.
Escape analysis determines all the places where a pointer can be stored and whether the lifetime of the pointer can be proven to be restricted only to the current procedure and/or threa.
C/C++中,有時(shí)為了提高效率,常常將pass-by-value(傳值)“升級(jí)”成pass-by-reference,企圖避免構(gòu)造函數(shù)的運(yùn)行,并且直接返回一個(gè)指針。然而這里隱藏了一個(gè)很大的坑:在函數(shù)內(nèi)部定義了一個(gè)局部變量,然后返回這個(gè)局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態(tài)內(nèi)存分配),一旦函數(shù)執(zhí)行完畢,變量占據(jù)的內(nèi)存會(huì)被銷毀,任何對(duì)這個(gè)返回值作的動(dòng)作(如解引用),都將擾亂程序的運(yùn)行,甚至導(dǎo)致程序直接崩潰。例如:
int *foo ( void )
{
int t = 3;
return t;
}
為了避免這個(gè)坑,有個(gè)更聰明的做法:在函數(shù)內(nèi)部使用new函數(shù)構(gòu)造一個(gè)變量(動(dòng)態(tài)內(nèi)存分配),然后返回此變量的地址。因?yàn)樽兞渴窃诙焉蟿?chuàng)建的,所以函數(shù)退出時(shí)不會(huì)被銷毀。但是,這樣就行了嗎?new出來(lái)的對(duì)象該在何時(shí)何地delete呢?調(diào)用者可能會(huì)忘記delete或者直接拿返回值傳給其他函數(shù),之后就再也不能delete它了,也就是發(fā)生了內(nèi)存泄露。關(guān)于這個(gè)坑,大家可以去看看《Effective C++》條款21,講得非常好!
C++是公認(rèn)的語(yǔ)法最復(fù)雜的語(yǔ)言,據(jù)說(shuō)沒(méi)有人可以完全掌握C++的語(yǔ)法。而這一切在Go語(yǔ)言中就大不相同了。像上面示例的C++代碼放到Go里,沒(méi)有任何問(wèn)題。
你表面的光鮮,一定是背后有很多人為你撐起的!Go語(yǔ)言里就是編譯器的逃逸分析。它是編譯器執(zhí)行靜態(tài)代碼分析后,對(duì)內(nèi)存管理進(jìn)行的優(yōu)化和簡(jiǎn)化。
在編譯原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析。通俗來(lái)講,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。
更簡(jiǎn)單來(lái)說(shuō),逃逸分析決定一個(gè)變量是分配在堆上還是分配在棧上。
二、 為什么要逃逸分析
前面講的C/C++中出現(xiàn)的問(wèn)題,在Go中作為一個(gè)語(yǔ)言特性被大力推崇。真是C/C++之砒霜Go之蜜糖!
C/C++中動(dòng)態(tài)分配的內(nèi)存需要我們手動(dòng)釋放,導(dǎo)致猿們平時(shí)在寫程序時(shí),如履薄冰。這樣做有他的好處:程序員可以完全掌控內(nèi)存。但是缺點(diǎn)也是很多的:經(jīng)常出現(xiàn)忘記釋放內(nèi)存,導(dǎo)致內(nèi)存泄露。所以,很多現(xiàn)代語(yǔ)言都加上了垃圾回收機(jī)制。
Go的垃圾回收,讓堆和棧對(duì)程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業(yè)務(wù),“高效”地完成代碼編寫。把那些內(nèi)存管理的復(fù)雜機(jī)制交給編譯器,而程序員可以去享受生活。
逃逸分析這種“騷操作”把變量合理地分配到它該去的地方,“找準(zhǔn)自己的位置”。即使你是用new申請(qǐng)到的內(nèi)存,如果我發(fā)現(xiàn)你竟然在退出函數(shù)后沒(méi)有用了,那么就把你丟到棧上,畢竟棧上的內(nèi)存分配比堆上快很多;反之,即使你表面上只是一個(gè)普通的變量,但是經(jīng)過(guò)逃逸分析后發(fā)現(xiàn)在退出函數(shù)之后還有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前實(shí)現(xiàn)共產(chǎn)主義!
如果變量都分配到堆上,堆不像棧可以自動(dòng)清理。它會(huì)引起Go頻繁地進(jìn)行垃圾回收,而垃圾回收會(huì)占用比較大的系統(tǒng)開銷(占用CPU容量的25%)。
堆和棧相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。棧內(nèi)存分配則會(huì)非??臁7峙鋬?nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過(guò)垃圾回收才能釋放。
通過(guò)逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會(huì)減輕分配堆內(nèi)存的開銷,同時(shí)也會(huì)減少gc的壓力,提高程序的運(yùn)行速度。
三、 逃逸分析如何完成
Go逃逸分析最基本的原則是:如果一個(gè)函數(shù)返回對(duì)一個(gè)變量的引用,那么它就會(huì)發(fā)生逃逸。
簡(jiǎn)單來(lái)說(shuō),編譯器會(huì)分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會(huì)再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語(yǔ)言里沒(méi)有一個(gè)關(guān)鍵字或者函數(shù)可以直接讓變量被編譯器分配到堆上,相反,編譯器通過(guò)分析代碼來(lái)決定將變量分配到何處。
對(duì)一個(gè)變量取地址,可能會(huì)被分配到堆上。但是編譯器進(jìn)行逃逸分析后,如果考察到在函數(shù)返回后,此變量不會(huì)被引用,那么還是會(huì)被分配到棧上。
簡(jiǎn)單來(lái)說(shuō),編譯器會(huì)根據(jù)變量是否被外部引用來(lái)決定是否逃逸:
1)如果函數(shù)外部沒(méi)有引用,則優(yōu)先放到棧中;
2) 如果函數(shù)外部存在引用,則必定放到堆中;
針對(duì)第一條,可能放到堆上的情形:定義了一個(gè)很大的數(shù)組,需要申請(qǐng)的內(nèi)存過(guò)大,超過(guò)了棧的存儲(chǔ)能力。
四、 逃逸分析實(shí)例
下面是一個(gè)簡(jiǎn)單的例子。
package main
import ()
func foo() *int {
var x int
return x
}
func bar() int {
x := new(int)
*x = 1
return *x
}
func main() {}
開啟逃逸分析日志很簡(jiǎn)單,只要在編譯的時(shí)候加上-gcflags '-m',但是我們?yōu)榱瞬蛔尵幾g時(shí)自動(dòng)內(nèi)連函數(shù),一般會(huì)加-l參數(shù),最終為-gcflags '-m -l',執(zhí)行如下命令:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:5:9: x escapes to heap
./main.go:4:6: moved to heap: x
./main.go:9:10: bar new(int) does not escape
上面代碼中foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了棧上。
也可以使用反匯編命令看出變量是否發(fā)生逃逸。
$ go tool compile -S main.go
截取部分結(jié)果,圖中標(biāo)記出來(lái)的說(shuō)明foo中x是在堆上分配內(nèi)存,發(fā)生了逃逸。

反匯編命令結(jié)果
什么時(shí)候逃逸呢? golang.org FAQ 上有一個(gè)關(guān)于變量分配的問(wèn)題如下:
Q: How do I know whether a variable is allocated on the heap or the stack?
A: From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
關(guān)于什么時(shí)候逃逸,什么時(shí)候不逃逸,我們接下來(lái)再看幾個(gè)小例子。
1)Example1
package main
type S struct{}
func main() {
var x S
y := x
_ = *identity(y)
}
func identity(z *S) *S {
return z
}
結(jié)果如下:
# command-line-arguments
./main.go:8:22: leaking param: z to result ~r1 level=0
./main.go:5:7: main x does not escape
這里的第一行表示z變量是“流式”,因?yàn)閕dentity這個(gè)函數(shù)僅僅輸入一個(gè)變量,又將這個(gè)變量作為返回輸出,但identity并沒(méi)有引用z,所以這個(gè)變量沒(méi)有逃逸,而x沒(méi)有被引用,且生命周期也在mian里,x沒(méi)有逃逸,分配在棧上。
2)Example2
package main
type S struct{}
func main() {
var x S
_ = *ref(x)
}
func ref(z S) *S {
return z
}
結(jié)果如下:
# command-line-arguments
./main.go:8:9: z escapes to heap
./main.go:7:16: moved to heap: z
這里的z是逃逸了,原因很簡(jiǎn)單,go都是值傳遞,ref函數(shù)copy了x的值,傳給z,返回z的指針,然后在函數(shù)外被引用,說(shuō)明z這個(gè)變量在函數(shù)內(nèi)聲明,可能會(huì)被函數(shù)外的其他程序訪問(wèn)。所以z逃逸了,分配在堆上
3)Example3
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(i)
}
func refStruct(y int) (z S) {
z.M = y
return z
}
結(jié)果如下:
# command-line-arguments
./main.go:10:8: y escapes to heap
./main.go:9:26: moved to heap: y
看日志的輸出,這里的y是逃逸了,看來(lái)在struct里好像并沒(méi)有區(qū)別,有可能被函數(shù)外的程序訪問(wèn)就會(huì)逃逸
4)Example4
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(i)
}
func refStruct(y *int) (z S) {
z.M = y
return z
}
結(jié)果如下:
# command-line-arguments
./main.go:9:27: leaking param: y to result z level=0
./main.go:7:12: main i does not escape
這里的y沒(méi)有逃逸,分配在棧上,原因和Example1是一樣的。
5)Example5
package main
type S struct {
M *int
}
func main() {
var x S
var i int
ref(i, x)
}
func ref(y *int, z *S) {
z.M = y
}
結(jié)果如下:
# command-line-arguments
./main.go:10:21: leaking param: y
./main.go:10:21: ref z does not escape
./main.go:8:6: i escapes to heap
./main.go:7:6: moved to heap: i
./main.go:8:10: main x does not escape
這里的z沒(méi)有逃逸,而i卻逃逸了,這是因?yàn)間o的逃逸分析不知道z和i的關(guān)系,逃逸分析不知道參數(shù)y是z的一個(gè)成員,所以只能把它分配給堆。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Golang極簡(jiǎn)入門教程(一):基本概念
- 理解Golang中的數(shù)組(array)、切片(slice)和map
- golang gorm 操作mysql及gorm基本用法
- golang語(yǔ)言中for循環(huán)語(yǔ)句用法實(shí)例
- golang中interface接口的深度解析
- Golang 中整數(shù)轉(zhuǎn)字符串的方法
- Golang讀寫Excel的方法教程
- Mac OS系統(tǒng)安裝golang教程
- golang判斷chan channel是否關(guān)閉的方法
- Golang的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù)的整理介紹