Golang之變量去哪兒?

寫過C/C++的同學(xué)都知道旺罢,調(diào)用著名的malloc和new函數(shù)可以在堆上分配一塊內(nèi)存旷余,這塊內(nèi)存的使用和銷毀的責(zé)任都在程序員。一不小心扁达,就會(huì)發(fā)生內(nèi)存泄露正卧,搞得膽戰(zhàn)心驚。

切換到Golang后跪解,基本不會(huì)擔(dān)心內(nèi)存泄露了炉旷。雖然也有new函數(shù),但是使用new函數(shù)得到的內(nèi)存不一定就在堆上叉讥。堆和棧的區(qū)別對(duì)程序員“模糊化”了窘行,當(dāng)然這一切都是Go編譯器在背后幫我們完成的。

一個(gè)變量是在堆上分配图仓,還是在棧上分配罐盔,是經(jīng)過編譯器的逃逸分析之后得出的結(jié)論。

這篇文章救崔,就將帶領(lǐng)大家一起去探索逃逸分析——變量到底去哪兒惶看,堆還是棧?

什么是逃逸分析

以前寫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;
} 

有些同學(xué)可能知道上面這個(gè)坑,用了個(gè)更聰明的做法:在函數(shù)內(nèi)部使用new函數(shù)構(gòu)造一個(gè)變量(動(dòng)態(tài)內(nèi)存分配)选脊,然后返回此變量的地址杭抠。因?yàn)樽兞渴窃诙焉蟿?chuàng)建的,所以函數(shù)退出時(shí)不會(huì)被銷毀恳啥。但是偏灿,這樣就行了嗎?new出來的對(duì)象該在何時(shí)何地delete呢钝的?調(diào)用者可能會(huì)忘記delete或者直接拿返回值傳給其他函數(shù)翁垂,之后就再也不能delete它了,也就是發(fā)生了內(nèi)存泄露硝桩。關(guān)于這個(gè)坑沿猜,大家可以去看看《Effective C++》條款21,講得非常好碗脊!

C++是公認(rèn)的語法最復(fù)雜的語言啼肩,據(jù)說沒有人可以完全掌握C++的語法。而這一切在Go語言中就大不相同了衙伶。像上面示例的C++代碼放到Go里祈坠,沒有任何問題。

你表面的光鮮矢劲,一定是背后有很多人為你撐起的赦拘!Go語言里就是編譯器的逃逸分析。它是編譯器執(zhí)行靜態(tài)代碼分析后芬沉,對(duì)內(nèi)存管理進(jìn)行的優(yōu)化和簡(jiǎn)化躺同。

在編譯原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析花嘶。通俗來講笋籽,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸椭员。

更簡(jiǎn)單來說车海,逃逸分析決定一個(gè)變量是分配在堆上還是分配在棧上。

為什么要逃逸分析

前面講的C/C++中出現(xiàn)的問題,在Go中作為一個(gè)語言特性被大力推崇侍芝。真是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)代語言都加上了垃圾回收機(jī)制。

Go的垃圾回收,讓堆和棧對(duì)程序員保持透明忌锯。真正解放了程序員的雙手箍鼓,讓他們可以專注于業(yè)務(wù),“高效”地完成代碼編寫。把那些內(nèi)存管理的復(fù)雜機(jī)制交給編譯器,而程序員可以去享受生活。

逃逸分析這種“騷操作”把變量合理地分配到它該去的地方御毅,“找準(zhǔn)自己的位置”。即使你是用new申請(qǐng)到的內(nèi)存怜珍,如果我發(fā)現(xiàn)你竟然在退出函數(shù)后沒有用了端蛆,那么就把你丟到棧上,畢竟棧上的內(nèi)存分配比堆上快很多绘面;反之欺税,即使你表面上只是一個(gè)普通的變量,但是經(jīng)過逃逸分析后發(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ì)非承痪荆快。棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASSE”,分配和釋放拨扶;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊凳鬓,之后要通過垃圾回收才能釋放。

通過逃逸分析患民,可以盡量把那些不需要分配到堆上的變量直接分配到棧上缩举,堆上的變量少了,會(huì)減輕分配堆內(nèi)存的開銷匹颤,同時(shí)也會(huì)減少gc的壓力仅孩,提高程序的運(yùn)行速度。

逃逸分析是怎么完成的

Go逃逸分析最基本的原則是:如果一個(gè)函數(shù)返回對(duì)一個(gè)變量的引用印蓖,那么它就會(huì)發(fā)生逃逸杠氢。

簡(jiǎn)單來說,編譯器會(huì)分析代碼的特征和代碼生命周期另伍,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會(huì)再被引用的,才分配到棧上绞旅,其他情況下都是分配到堆上摆尝。

Go語言里沒有一個(gè)關(guān)鍵字或者函數(shù)可以直接讓變量被編譯器分配到堆上,相反因悲,編譯器通過分析代碼來決定將變量分配到何處堕汞。

對(duì)一個(gè)變量取地址,可能會(huì)被分配到堆上晃琳。但是編譯器進(jìn)行逃逸分析后讯检,如果考察到在函數(shù)返回后,此變量不會(huì)被引用卫旱,那么還是會(huì)被分配到棧上人灼。套個(gè)取址符,就想騙補(bǔ)助顾翼?Too young投放!

簡(jiǎn)單來說,編譯器會(huì)根據(jù)變量是否被外部引用來決定是否逃逸:

  1. 如果函數(shù)外部沒有引用适贸,則優(yōu)先放到棧中灸芳;
  2. 如果函數(shù)外部存在引用,則必定放到堆中拜姿;

針對(duì)第一條烙样,可能放到堆上的情形:定義了一個(gè)很大的數(shù)組,需要申請(qǐng)的內(nèi)存過大蕊肥,超過了棧的存儲(chǔ)能力谒获。

逃逸分析實(shí)例

Go提供了相關(guān)的命令,可以查看變量是否發(fā)生逃逸。

還是用上面我們提到的例子:

package main

import "fmt"

func foo() *int {
    t := 3
    return &t;
}

func main() {
    x := foo()
    fmt.Println(*x)
}

foo函數(shù)返回一個(gè)局部變量的指針究反,main函數(shù)里變量x接收它寻定。執(zhí)行如下命令:

go build -gcflags '-m -l' main.go

-l是為了不讓foo函數(shù)被內(nèi)聯(lián)。得到如下輸出:

# 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函數(shù)里的變量t逃逸了精耐,和我們預(yù)想的一致狼速。讓我們不解的是為什么main函數(shù)里的x也逃逸了?這是因?yàn)橛行┖瘮?shù)參數(shù)為interface類型卦停,比如fmt.Println(a ...interface{})向胡,編譯期間很難確定其參數(shù)的具體類型,也會(huì)發(fā)生逃逸惊完。

使用反匯編命令也可以看出變量是否發(fā)生逃逸僵芹。

go tool compile -S main.go

截取部分結(jié)果,圖中標(biāo)記出來的說明t是在堆上分配內(nèi)存小槐,發(fā)生了逃逸拇派。

反匯編

總結(jié)

堆上動(dòng)態(tài)分配內(nèi)存比棧上靜態(tài)分配內(nèi)存,開銷大很多凿跳。

變量分配在棧上需要能在編譯期確定它的作用域件豌,否則會(huì)分配到堆上。

Go編譯器會(huì)在編譯期對(duì)考察變量的作用域控嗜,并作一系列檢查茧彤,如果它的作用域在運(yùn)行期間對(duì)編譯器一直是可知的,那么就會(huì)分配到棧上疆栏。

簡(jiǎn)單來說曾掂,編譯器會(huì)根據(jù)變量是否被外部引用來決定是否逃逸。對(duì)于Go程序員來說壁顶,編譯器的這些逃逸分析規(guī)則不需要掌握珠洗,我們只需通過go build -gcflags '-m'命令來觀察變量逃逸情況就行了。

不要盲目使用變量的指針作為函數(shù)參數(shù)博助,雖然它會(huì)減少?gòu)?fù)制操作险污。但其實(shí)當(dāng)參數(shù)為變量自身的時(shí)候,復(fù)制是在棧上完成的操作富岳,開銷遠(yuǎn)比變量逃逸后動(dòng)態(tài)地在堆上分配內(nèi)存少的多蛔糯。

最后,盡量寫出少一些逃逸的代碼窖式,提升程序的運(yùn)行效率蚁飒。

QR

參考資料

【逃逸是怎么發(fā)生的?很贊 結(jié)尾有很多參考資料】https://www.do1618.com/archives/1328/go-%E5%86%85%E5%AD%98%E9%80%83%E9%80%B8%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/

【Go的變量到底在堆還是棧中分配】https://github.com/developer-learning/night-reading-go/blob/master/content/discuss/2018-07-09-make-new-in-go.md

【Golang堆棧的理解】https://segmentfault.com/a/1190000017498101

【逃逸分析 編寫棧分配內(nèi)存建議】https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
【逃逸分析 比較簡(jiǎn)潔】https://studygolang.com/articles/17584

【逃逸分析定義】https://cloud.tencent.com/developer/article/1117410

【逃逸分析例子】https://my.oschina.net/renhc/blog/2222104

https://gocn.vip/article/355
【匯編代碼 傳參】https://github.com/maniafish/about_go/blob/master/heap_stack.md

【逃逸分析的缺陷】https://studygolang.com/articles/12396

【比較好的逃逸分析的例子】http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末萝喘,一起剝皮案震驚了整個(gè)濱河市淮逻,隨后出現(xiàn)的幾起案子琼懊,更是在濱河造成了極大的恐慌,老刑警劉巖爬早,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哼丈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡筛严,警方通過查閱死者的電腦和手機(jī)醉旦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桨啃,“玉大人车胡,你說我怎么就攤上這事≌振” “怎么了匈棘?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)析命。 經(jīng)常有香客問我主卫,道長(zhǎng),這世上最難降的妖魔是什么鹃愤? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任队秩,我火速辦了婚禮,結(jié)果婚禮上昼浦,老公的妹妹穿的比我還像新娘。我一直安慰自己筒主,他們只是感情好关噪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乌妙,像睡著了一般使兔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上藤韵,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天虐沥,我揣著相機(jī)與錄音,去河邊找鬼泽艘。 笑死欲险,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的匹涮。 我是一名探鬼主播天试,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼然低!你這毒婦竟也來了喜每?” 一聲冷哼從身側(cè)響起务唐,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎带兜,沒想到半個(gè)月后枫笛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刚照,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年刑巧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涩咖。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡海诲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出檩互,到底是詐尸還是另有隱情特幔,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布闸昨,位于F島的核電站蚯斯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饵较。R本人自食惡果不足惜拍嵌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望循诉。 院中可真熱鬧横辆,春花似錦、人聲如沸茄猫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)划纽。三九已至脆侮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勇劣,已是汗流浹背靖避。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留比默,地道東北人幻捏。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像命咐,于是被迫代替她去往敵國(guó)和親粘咖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,092評(píng)論 1 32
  • 1. 基礎(chǔ)知識(shí) 1.1侈百、 基本概念瓮下、 功能 馮諾伊曼體系結(jié)構(gòu)1翰铡、計(jì)算機(jī)處理的數(shù)據(jù)和指令一律用二進(jìn)制數(shù)表示2、順序執(zhí)...
    yunpiao閱讀 5,269評(píng)論 1 22
  • 第二部分 自動(dòng)內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,138評(píng)論 0 2
  • 1、在IDE中創(chuàng)建腳本比較方便路呜。由于大部分時(shí)間我們都是在編輯組件腳本迷捧,所以這里就圍繞組件腳本來展開。 2胀葱、coco...
    mjwz5294閱讀 3,755評(píng)論 0 3
  • 匆匆忙忙奔波 糾葛纏絲喜怒 停留在昨天的思想 趕不及今天的時(shí)光 情愿也好不情愿也好 太陽(yáng)再次升起的時(shí)候 你已經(jīng)離回...
    營(yíng)州布衣閱讀 265評(píng)論 1 4