一文帶你搞懂golang中內(nèi)存分配逃逸分析
一. golang 的內(nèi)存分配逃逸
1. 關(guān)于堆和棧
棧 可以簡單得理解成一次函數(shù)調(diào)用內(nèi)部申請到的內(nèi)存,它們會隨著函數(shù)的返回把內(nèi)存還給系統(tǒng)。
func F() { temp := make([]int, 0, 20) ... }
類似于上面代碼里面的temp變量,只是內(nèi)函數(shù)內(nèi)部申請的臨時(shí)變量,并不會作為返回值返回,它就是被編譯器申請到棧里面。
申請到 棧內(nèi)存 好處:函數(shù)返回直接釋放,不會引起垃圾回收,對性能沒有影響。
再來看看堆得情況之一如下代碼:
func F() []int{ a := make([]int, 0, 20) return a }
而上面這段代碼,申請的代碼一模一樣,但是申請后作為返回值返回了,編譯器會認(rèn)為變量之后還會被使用,當(dāng)函數(shù)返回之后并不會將其內(nèi)存歸還,那么它就會被申請到 堆 上面了。
申請到堆上面的內(nèi)存才會引起垃圾回收,如果這個(gè)過程(特指垃圾回收不斷被觸發(fā))過于高頻就會導(dǎo)致 gc 壓力過大,程序性能出問題。
我們再看看如下幾個(gè)例子:
func F() { a := make([]int, 0, 20) // 棧 空間小 b := make([]int, 0, 20000) // 堆 空間過大 l := 20 c := make([]int, 0, l) // 堆 動態(tài)分配不定空間 }
像是 b 這種 即使是臨時(shí)變量,申請過大也會在堆上面申請。
對于 c 編譯器對于這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。
2. 逃逸分析(Escape analysis)
所謂逃逸分析(Escape analysis)是指由編譯器決定內(nèi)存分配的位置,不需要程序員指定。
在函數(shù)中申請一個(gè)新的對象:
- 如果分配 在棧中,則函數(shù)執(zhí)行結(jié)束可自動將內(nèi)存回收;
- 如果分配在堆中,則函數(shù)執(zhí)行結(jié)束可交給GC(垃圾回收)處理;
注意,對于函數(shù)外部沒有引用的對象,也有可能放到堆中,比如內(nèi)存過大超過棧的存儲能力。
3. 逃逸場景(什么情況才分配到堆中)
3.1 指針逃逸
Go可以返回局部變量指針,這其實(shí)是一個(gè)典型的變量逃逸案例,示例代碼如下:
package main type Student struct { Name string Age int } func StudentRegister(name string, age int) *Student { s := new(Student) //局部變量s逃逸到堆 s.Name = name s.Age = age return s } func main() { StudentRegister("Jim", 18) }
雖然 在函數(shù) StudentRegister() 內(nèi)部 s 為局部變量,其值通過函數(shù)返回值返回,s 本身為一指針,其指向的內(nèi)存地址不會是棧而是堆,這就是典型的逃逸案例。
終端運(yùn)行命令查看逃逸分析日志:
go build -gcflags=-m
可見在StudentRegister()函數(shù)中,也即代碼第9行顯示”escapes to heap”,代表該行內(nèi)存分配發(fā)生了逃逸現(xiàn)象。
3.2 棧空間不足逃逸(空間開辟過大)
package main func Slice() { s := make([]int, 1000, 1000) for index, _ := range s { s[index] = index } } func main() { Slice() }
上面代碼Slice()函數(shù)中分配了一個(gè)1000個(gè)長度的切片,是否逃逸取決于棧空間是否足夠大。 直接查看編譯提示,如下:
所以只是1000的長度還不足以發(fā)生逃逸現(xiàn)象。然后就x10倍吧
package main func Slice() { s := make([]int, 10000, 10000) for index, _ := range s { s[index] = index } } func main() { Slice() }
分析如下:
當(dāng)切片長度擴(kuò)大到10000時(shí)就會逃逸。
實(shí)際上當(dāng)??臻g不足以存放當(dāng)前對象時(shí)或無法判斷當(dāng)前切片長度時(shí)會將對象分配到堆中。
3.3 動態(tài)類型逃逸(不確定長度大?。?/strong>
很多函數(shù)參數(shù)為interface類型,比如fmt.Println(a …interface{}),編譯期間很難確定其參數(shù)的具體類型,也能產(chǎn)生逃逸。
如下代碼所示:
package main import "fmt" func main() { s := "Escape" fmt.Println(s) }
逃逸分下如下:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap
.\main.go:7: main ... argument does not escape
又或者像前面提到的例子:
func F() { a := make([]int, 0, 20) // 棧 空間小 b := make([]int, 0, 20000) // 堆 空間過大 逃逸 l := 20 c := make([]int, 0, l) // 堆 動態(tài)分配不定空間 逃逸 }
3.4 閉包引用對象逃逸
Fibonacci數(shù)列的函數(shù):
package main import "fmt" func Fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { f := Fibonacci() for i := 0; i < 10; i++ { fmt.Printf("Fibonacci: %d\n", f()) } }
輸出如下:
~/go/src/gitHub/test/pool go run main.go
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
Fibonacci: 55
逃逸如下:
~/go/src/gitHub/test/pool go build -gcflags=-m
# gitHub/test/pool
./main.go:7:9: can inline Fibonacci.func1
./main.go:7:9: func literal escapes to heap
./main.go:7:9: func literal escapes to heap
./main.go:8:10: &b escapes to heap
./main.go:6:5: moved to heap: b
./main.go:8:13: &a escapes to heap
./main.go:6:2: moved to heap: a
./main.go:17:34: f() escapes to heap
./main.go:17:13: main ... argument does not escape
Fibonacci()函數(shù)中原本屬于局部變量的a和b由于閉包的引用,不得不將二者放到堆上,以致產(chǎn)生逃逸。
逃逸分析的作用是什么呢?
- 逃逸分析的好處是為了減少gc的壓力,不逃逸的對象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需要gc標(biāo)記清除。
- 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配 ,而沒有發(fā)生逃逸的則有編譯器在棧上分配)。
- 同步消除,如果你定義的對象的方法上有同步鎖,但在運(yùn)行時(shí),卻只有一個(gè)線程在訪問,此時(shí)逃逸分析后的機(jī)器碼,會去掉同步鎖運(yùn)行。
逃逸總結(jié):
- 棧上分配內(nèi)存比在堆中分配內(nèi)存有更高的效率
- 棧上分配的內(nèi)存不需要GC處理
- 堆上分配的內(nèi)存使用完畢會交給GC處理
- 逃逸分析目的是決定內(nèi)分配地址是棧還是堆
- 逃逸分析在編譯階段完成
提問:函數(shù)傳遞指針真的比傳值效率高嗎?
我們知道傳遞指針可以減少底層值的拷貝,可以提高效率,但是如果拷貝的數(shù)據(jù)量小,由于指針傳遞會產(chǎn)生逃逸,可能會使用堆,也可能會增加GC的負(fù)擔(dān),所以傳遞指針不一定是高效的。
在官網(wǎng) (golang.org) FAQ 上有一個(gè)關(guān)于變量分配的問題如下:
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.
翻譯如下:
如何得知變量是分配在棧(stack)上還是堆(heap)上?
準(zhǔn)確地說,你并不需要知道。Golang 中的變量只要被引用就一直會存活,存儲在堆上還是棧上由內(nèi)部實(shí)現(xiàn)決定而和具體的語法沒有關(guān)系。
知道變量的存儲位置確實(shí)和效率編程有關(guān)系。如果可能,Golang 編譯器會將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。 然而,如果編譯器不能確保變量在函數(shù) return之后不再被引用,編譯器就會將變量分配到堆上。而且,如果一個(gè)局部變量非常大,那么它也應(yīng)該被分配到堆上而不是棧上。
當(dāng)前情況下,如果一個(gè)變量被取地址,那么它就有可能被分配到堆上。然而,還要對這些變量做逃逸分析,如果函數(shù)return之后,變量不再被引用,則將其分配到棧上。
二. golang 臨時(shí)對象池sync.Pool
1. 內(nèi)存碎片化問題
實(shí)際項(xiàng)目基本都是通過
c := make([]int, 0, l)
來申請內(nèi)存,長度都是不確定的,自然而然這些變量都會申請到堆上面了。
Golang使用的垃圾回收算法是『標(biāo)記——清除』。
簡單得說,就是程序要從操作系統(tǒng)申請一塊比較大的內(nèi)存,內(nèi)存分成小塊,通過鏈表鏈接。
每次程序申請內(nèi)存,就從鏈表上面遍歷每一小塊,找到符合的就返回其地址,沒有合適的就從操作系統(tǒng)再申請。如果申請內(nèi)存次數(shù)較多,而且申請的大小不固定,就會引起內(nèi)存碎片化的問題。
申請的堆內(nèi)存并沒有用完,但是用戶申請的內(nèi)存的時(shí)候卻沒有合適的空間提供。這樣會遍歷整個(gè)鏈表,還會繼續(xù)向操作系統(tǒng)申請內(nèi)存。這就能解釋我一開始描述的問題,申請一塊內(nèi)存變成了慢語句。
到此這篇關(guān)于一文帶你搞懂golang中內(nèi)存分配逃逸分析的文章就介紹到這了,更多相關(guān)go內(nèi)存分配逃逸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go?time.Sleep睡眠指定時(shí)間實(shí)例詳解(小時(shí)級到納秒級)
golang的休眠可以使用time包中的sleep,下面這篇文章主要給大家介紹了關(guān)于go?time.Sleep睡眠指定時(shí)間(小時(shí)級到納秒級)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11關(guān)于go語言載入json可能遇到的一個(gè)坑
Go 語言從新手到大神,每個(gè)人多少都會踩一些坑,那么下面這篇文章主要給大家介紹了關(guān)于go語言載入json可能遇到的一個(gè)坑,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-07-07Golang程序漏洞檢測器govulncheck的安裝和使用
govulncheck 是一個(gè)命令行工具,可以幫助 Golang 開發(fā)者快速找到項(xiàng)目代碼和依賴的模塊中的安全漏洞,該工具可以分析源代碼和二進(jìn)制文件,識別代碼中對這些漏洞的任何直接或間接調(diào)用,本文就給大家介紹一下govulncheck安裝和使用,需要的朋友可以參考下2023-09-09golang?pprof?監(jiān)控系列?go?trace統(tǒng)計(jì)原理與使用解析
這篇文章主要為大家介紹了golang?pprof?監(jiān)控系列?go?trace統(tǒng)計(jì)原理與使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04