您好,登錄后才能下訂單哦!
這篇文章主要介紹了Go語言內(nèi)存逃逸是什么的相關(guān)知識,內(nèi)容詳細(xì)易懂,操作簡單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Go語言內(nèi)存逃逸是什么文章都會有所收獲,下面我們一起來看看吧。
我們在高中學(xué)過一些天體物理的知識,比如常見的三個(gè)宇宙速度:
第一宇宙速度:航天器逃離地面圍繞地球做圓周運(yùn)動的最小速度:7.9km/s
第二宇宙速度:航天器逃離地球的最小速度:11.18km/s
第三宇宙速度:航天器逃離太陽系的最小速度:16.64km/s
了解了航天器的逃逸行為,我們今天來點(diǎn)特別的:內(nèi)存逃逸。
通過本文你將了解到以下內(nèi)容:
C/C++的內(nèi)存布局和堆棧
Go的內(nèi)存逃逸和逃逸分析
內(nèi)存逃逸的小結(jié)
這應(yīng)該是一道出現(xiàn)頻率極高的面試題。
C/C++作為靜態(tài)強(qiáng)類型語言,編譯成二進(jìn)制文件后,運(yùn)行時(shí)整個(gè)程序的內(nèi)存空間分為:
內(nèi)核空間 Kernel Space
用戶空間 User Space
內(nèi)核空間主要存放進(jìn)程運(yùn)行時(shí)的一些控制信息,用戶空間則是存放程序本身的一些信息,我們來看下用戶空間的布局:
堆和棧的主要特點(diǎn):
棧區(qū)(Stack):由編譯器自動分配釋放,存儲函數(shù)的參數(shù)值,局部變量值等,但是空間一般較小數(shù)KB~數(shù)MB
堆區(qū)(Heap):C/C++沒有GC機(jī)制,堆內(nèi)存一般由程序員申請和釋放,空間較大,能否用好取決于使用者的水平
Go語言與C語言淵源極深,C語言面臨的問題,Go同樣會面對,比如:變量的內(nèi)存分配問題。
在C語言中,需要程序員自己根據(jù)需要來確定采用堆還是棧,棧內(nèi)存由OS全權(quán)負(fù)責(zé),但是堆內(nèi)存需要顯式調(diào)用malloc/new等函數(shù)申請,并且對應(yīng)調(diào)用free/delete來釋放。
Go語言具有垃圾回收Garbage Collection機(jī)制來進(jìn)行堆內(nèi)存管理,并且沒有像malloc/new這種堆內(nèi)存分配的關(guān)鍵字。
棧內(nèi)存的分配和釋放開銷非常小,堆內(nèi)存對于Go來說開銷比棧內(nèi)存大很多。
如果寫過C/C++都會知道,在函數(shù)內(nèi)部聲明局部變量,然后返回其指針,如果外部調(diào)用則會報(bào)錯(cuò):
#include <iostream> using namespace std; int* getValue() { int val = 10086; return &val; } int main() { cout<<*getValue()<<endl; return 0; }
編譯上述代碼:main.cpp: In function ‘int* getValue()’: main.cpp:7:9: warning: address of local variable ‘val’ returned [-Wreturn-local-addr]
用同樣的思想,寫一個(gè)go版本的代碼:
package main import ( "fmt" ) func main() { str := GetString() fmt.Println(*str) } func GetString() *string { var s string s = "hello world" return &s }
代碼卻可以正常運(yùn)行,我們本意是在棧上分配一個(gè)變量,用完就銷毀,但是外部卻調(diào)用了,甚至可以正常進(jìn)行,表現(xiàn)和C++完全不同。
其實(shí),這就是Go的內(nèi)存逃逸現(xiàn)象,Go模糊了棧內(nèi)存和堆內(nèi)存的界限,具體來說變量究竟分配到哪里,是由編譯器來決定的。
1逃逸分析escape analysis
所謂逃逸分析就是在編譯階段由編譯器根據(jù)變量的類型、外部使用情況等因素來判定是分配到堆還是棧,從而替代人工處理。
一般將局部變量和參數(shù)分配到棧上,但是并不絕對:
如果編譯器不能確定在函數(shù)返回時(shí),變量是否被使用則分配到堆上
如果局部變量非常大,也會分配到堆上
......
編譯器不清楚局部變量是否會被外部使用時(shí),就會傾向于分配到堆上。
Go編譯器在確定函數(shù)返回后不會再被引用時(shí)才分配到棧上,其他情況下都是分配到堆上。
這樣做雖然浪費(fèi)堆空間,但是有效避免了懸掛指針的出現(xiàn),并且由于GC的存在也不會出現(xiàn)內(nèi)存泄漏,權(quán)衡之下也是一種合理的做法。
2哪些情況會出現(xiàn)內(nèi)存逃逸
對于Go來說,在日常使用中有幾種常見的做法會導(dǎo)致內(nèi)存逃逸現(xiàn)象的出現(xiàn):
指針逃逸
??臻g不足逃逸
map/slice/interface/channel的使用
......
指針逃逸
在上一個(gè)例子中我們使用一個(gè)int指針來說明內(nèi)存逃逸的現(xiàn)象,接下來我們擴(kuò)展一下變?yōu)榻Y(jié)構(gòu)體指針,并且使用gcflags來給編譯器傳特定參數(shù)來觀察逃逸現(xiàn)象:
// test.go package main import "fmt" type Escape struct { who string } func CallInstance(caller string) (*Escape) { instance := new(Escape) instance.who = caller return instance } func main() { outer := CallInstance("hello world") fmt.Println(outer.who) }
執(zhí)行:go build -gcflags=-m test.go 如下:
# command-line-arguments ./test.go:9:6: can inline CallInstance ./test.go:16:23: inlining call to CallInstance ./test.go:17:13: inlining call to fmt.Println ./test.go:9:19: leaking param: caller ./test.go:10:17: new(Escape) escapes to heap ./test.go:16:23: main new(Escape) does not escape ./test.go:17:19: outer.who escapes to heap ./test.go:17:13: main []interface {} literal does not escape ./test.go:17:13: io.Writer(os.Stdout) escapes to heap <autogenerated>:1: (*File).close .this does not escape
我們可以看到"escapes to heap",確實(shí)出現(xiàn)了內(nèi)存逃逸,本該在棧上逃逸到堆上了。
棧空間不足逃逸
對于64bit的Linux系統(tǒng)而言棧的大小一般是8MB,Go中每個(gè)goroutine初始化棧大小是2KB,在goroutine的運(yùn)行過程中棧的大小可能會變化,但也不會超過OS對線程棧大小的限制。
在網(wǎng)上找了個(gè)例子,用mac跑了一下:
package main import "math/rand" func generate8191() { nums := make([]int, 8191) // < 64KB for i := 0; i < 8191; i++ { nums[i] = rand.Int() } } func generate8192() { nums := make([]int, 8192) // = 64KB for i := 0; i < 8192; i++ { nums[i] = rand.Int() } } func generate(n int) { nums := make([]int, n) // 不確定大小 for i := 0; i < n; i++ { nums[i] = rand.Int() } } func main() { generate8191() generate8192() generate(1) }
# command-line-arguments ./test_3.go:6:14: generate8191 make([]int, 8191) does not escape ./test_3.go:13:14: make([]int, 8192) escapes to heap ./test_3.go:20:14: make([]int, n) escapes to heap
可以看到在分配8191個(gè)大小時(shí)未發(fā)生逃逸,在分配8192時(shí)發(fā)生了逃逸,不定長度也發(fā)生了逃逸。
其他情況
在go中map、interface、slice、interface是非常常見的數(shù)據(jù)結(jié)構(gòu),也是非常容易觸發(fā)內(nèi)存逃逸的根源。
向channel中發(fā)送指針或者帶指針的值,因?yàn)樵诰幾g時(shí)沒有辦法知道哪個(gè)goroutine會在channel上接收數(shù)據(jù)。所以編譯器沒法知道變量什么時(shí)候才會被釋放。
slice中指針或帶指針的值,這會導(dǎo)致切片的內(nèi)容逃逸,盡管其后面的數(shù)組可能是在棧上分配的,但其引用的值一定是在堆上。
slice數(shù)組擴(kuò)容也可能導(dǎo)致內(nèi)存逃逸,如果切片背后的存儲要基于運(yùn)行時(shí)的數(shù)據(jù)進(jìn)行擴(kuò)充,就會在堆上分配。
interface類型可以代表任意類型,編譯器不知道參數(shù)會是什么類型,只有運(yùn)行時(shí)才知道,因此只能分配到堆上。
我們該如何評價(jià)內(nèi)存逃逸呢?
Go語言對用戶來說模糊了堆內(nèi)存和棧內(nèi)存的分配,編譯器借助于逃逸分析來實(shí)現(xiàn)特定場景的內(nèi)存逃逸。
任何事情都是兩面性,Go語言借助于內(nèi)存逃逸和GC機(jī)制解放了程序員,但是同時(shí)也帶來了性能問題,因?yàn)槎褍?nèi)存的分配和釋放都是需要成本的。
Go的編譯器在很多時(shí)候無法確定該如何分配內(nèi)存,因此只能采用一種穩(wěn)妥但有失性能的做法,分配到堆上。
意識里指針傳遞比值傳遞更高效,但是在Go中并非如此,如果指針傳遞出現(xiàn)內(nèi)存逃逸將內(nèi)存分配到堆上后續(xù)就有會GC操作,消耗比值傳遞更大。
如果明確不需要外部使用,就需要盡量避免內(nèi)存逃逸,不要一味完全依賴編譯器本身。
關(guān)于“Go語言內(nèi)存逃逸是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“Go語言內(nèi)存逃逸是什么”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。