什么是內(nèi)存周蹭?什么是內(nèi)存逃逸趋艘?怎么做內(nèi)存逃逸分析

內(nèi)存

  • 平時(shí)我們?cè)陔娔X上聽歌疲恢,聊天,或者啟動(dòng)某個(gè)程序瓷胧,那么這個(gè)啟動(dòng)過程显拳,其實(shí)就是把程序從硬盤讀入到內(nèi)存中去。就像安卓手機(jī)搓萧,內(nèi)存不夠了很卡杂数,殺掉幾個(gè)軟件,內(nèi)存就升上來了瘸洛。但也不是所有的程序都會(huì)一次性的讀入內(nèi)存揍移,為了節(jié)省內(nèi)存空間和提高效率,程序是可用分段或者分頁的加載反肋,比如一個(gè)2k內(nèi)存的機(jī)器讀一個(gè)2m的文件羊精。

什么是內(nèi)存呢

我們知道,CPU計(jì)算很快囚玫,但是磁盤的IO實(shí)在是太慢了喧锦。解決CPU和磁盤之間速度的鴻溝,我們引入了內(nèi)存抓督。其實(shí)在CPU內(nèi)部還有一部分緩存燃少。我們先來看一下計(jì)算機(jī)的存儲(chǔ)設(shè)備有哪些。
[圖片上傳失敗...(image-154fa6-1683545907588)]
我們?cè)倭炕幌逻@些存儲(chǔ)設(shè)備的速度铃在,大概是這樣

  • CPU : 每個(gè)指令大概需要 0.38ns阵具,以此作為對(duì)比的基本單位 1s
  • 一級(jí)緩存:讀取時(shí)間大約為 0.5ns,對(duì)比 CPU 的時(shí)間大約是 1.3s
  • CPU 分支預(yù)測錯(cuò)誤: 耗時(shí)為 5ns定铜,對(duì)比 CPU 的時(shí)間大約是 13s
  • 二級(jí)緩存:讀取時(shí)間大約為 7ns阳液,對(duì)比 CPU 的時(shí)間大約是 18.2s(與一級(jí)緩存相差了一個(gè)數(shù)量級(jí))
  • 鎖:互斥鎖的加鎖和解鎖大約需要 25ns,對(duì)比 CPU 的時(shí)間大約是 65s(一分鐘)揣炕。所以說帘皿,在并發(fā)編程中,鎖是一個(gè)很耗時(shí)的操作
  • 內(nèi)存:每次內(nèi)存尋址需要 100ns畸陡,對(duì)比 CPU 的時(shí)間大約是 260s(四分鐘鹰溜,又提升了一個(gè)數(shù)量級(jí))。CPU 和內(nèi)存之間的瓶頸被稱為馮諾依曼瓶頸
  • 一次 CPU 上下文切換:大約耗時(shí)為 1500ns丁恭,對(duì)比 CPU 的時(shí)間大約是 65 分鐘(一個(gè)小時(shí))曹动。在上下文切換的時(shí)間內(nèi)萄金,CPU 沒有做任何有用的計(jì)算书在,只是切換了兩個(gè)不同進(jìn)程的寄存器和內(nèi)存狀態(tài)。
  • 在 1Gbps 的網(wǎng)絡(luò)上傳輸 2k 的數(shù)據(jù)需要 20us埋嵌,對(duì)比 CPU 的時(shí)間大約是 14.4 個(gè)小時(shí)(理論值,實(shí)際中可能更久)贡必,可以看到網(wǎng)絡(luò)上非常少的數(shù)據(jù)傳輸對(duì)于 CPU 來說已經(jīng)很漫長了
  • SSD 隨機(jī)讀取耗時(shí)為 150us熬的,對(duì)比 CPU 的時(shí)間為 4.5 天。SSD 的速度已經(jīng)比機(jī)械硬盤快很多了赊级,但對(duì)于 CPU 來說速度就想烏龜一樣押框。所以應(yīng)該少寫 I/O 設(shè)備讀取的代碼,把常用的數(shù)據(jù)放到內(nèi)存中作為緩存理逊。
  • 從內(nèi)存中讀取1MB 的連續(xù)數(shù)據(jù)橡伞,耗時(shí)大約是 250us,對(duì)比 CPU 的時(shí)間是 7.5 天
  • 同一個(gè)數(shù)據(jù)中心網(wǎng)絡(luò)上跑一個(gè)來回需要 0.5ms晋被,對(duì)比 CPU 的時(shí)間大約是 15 天(半個(gè)月)兑徘。
  • 從 SSD 讀取 1MB 的順序數(shù)據(jù),大約學(xué)院 1ms羡洛,對(duì)比 CPU 的時(shí)間大約是一個(gè)月
  • 磁盤尋址時(shí)間是 10ms挂脑,對(duì)比 CPU 的時(shí)間是 10 個(gè)月
  • 從磁盤讀取 1MB 的連續(xù)數(shù)據(jù)需要 20ms,對(duì)比 CPU 的時(shí)間是 20 個(gè)月欲侮。所以說IO 設(shè)備是計(jì)算機(jī)系統(tǒng)的瓶頸
  • 從世界上不同城市的網(wǎng)絡(luò)上走一個(gè)來回崭闲,平均需要 150ms,對(duì)比 CPU 的時(shí)間是 12.5 年威蕉。所以程序和架構(gòu)都會(huì)盡量避免不同城市或者是跨國家的網(wǎng)絡(luò)訪問
  • 虛擬機(jī)重啟一次需要 4s 的時(shí)間刁俭,對(duì)比 CPU 的時(shí)間是三百多年,
  • 物理服務(wù)器重啟一次的時(shí)間是5min韧涨,對(duì)比 CPU 的時(shí)間是2萬5千年

那么為什么我們不能全部用最高速的存儲(chǔ)設(shè)備呢牍戚?因?yàn)樵娇拷?CPU 速度越快,容量越小虑粥,價(jià)格越貴如孝。哈哈。
另外每一種存儲(chǔ)器設(shè)備只和它相鄰的存儲(chǔ)設(shè)備打交道娩贷。比如第晰,CPU Cache 是從內(nèi)存里加載而來的,或者需要寫回內(nèi)存育勺,并不會(huì)直接寫回?cái)?shù)據(jù)到硬盤但荤,也不會(huì)直接從硬盤加載數(shù)據(jù)到 CPU Cache 中,而是先加載到內(nèi)存涧至,再從內(nèi)存加載到 Cache 中。

  • 在linux下桑包,我們可以通過以下命令查看高速緩存的大小
# cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
# cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
# cat /sys/devices/system/cpu/cpu0/cache/index2/size
4096K

內(nèi)存逃逸

  • go程序中的數(shù)據(jù)和變量都會(huì)被分配到程序所在擁有的內(nèi)存中南蓬。而內(nèi)存中有兩個(gè)重要的區(qū)域,就是棧區(qū)(Stack)和堆區(qū)(Heap)。

棧區(qū)的內(nèi)存一般由編譯器自動(dòng)進(jìn)行分配和釋放赘方,其中存儲(chǔ)著函數(shù)的入?yún)⒁约熬植孔兞可沼保@些參數(shù)會(huì)隨著函數(shù)的創(chuàng)建而 創(chuàng)建,函數(shù)的返回而消亡窄陡,一般不會(huì)在程序中長期存在炕淮,這種線性的內(nèi)存分配策略有著極高地效率,但是工程師也往 往不能控制棧內(nèi)存的分配跳夭,這部分工作基本都是由編譯器自動(dòng)完成的涂圆。

一般來講堆是人為手動(dòng)進(jìn)行管理,手動(dòng)申請(qǐng)币叹、分配润歉、釋放。一般硬件內(nèi)存有多大堆內(nèi)存就有多大颈抚。適合不可預(yù)知大小的內(nèi)存分配踩衩,分配速度較慢,而且會(huì)形成內(nèi)存碎片贩汉。C++ 等編程語言會(huì)由工程師主動(dòng)申請(qǐng)和釋放內(nèi)存驱富,Go 以及 Java 等編程語言 會(huì)由工程師和編譯器共同管理,堆中的對(duì)象由內(nèi)存分配器分配并由垃圾收集器回收匹舞。

什么是內(nèi)存逃逸

當(dāng)編譯器無法保證一個(gè)變量的生命周期只在函數(shù)內(nèi)部時(shí)萌朱,它就會(huì)認(rèn)為這個(gè)變量逃逸了,需要在堆上分配內(nèi)存策菜。這樣可以保證變量在函數(shù)返回后仍然有效晶疼,不會(huì)被棧回收又憨。簡單來說翠霍,局部變量通過堆分配和回收,就叫內(nèi)存逃逸蠢莺。在程序中寒匙,每個(gè)函數(shù)塊都會(huì)有自己的內(nèi)存區(qū)域用來存自己的局部變量(內(nèi)存占用少)、返回地址躏将、返回值之類的數(shù)據(jù)锄弱,這一塊內(nèi)存區(qū)域有特定的結(jié)構(gòu)和尋址方式,尋址起來十分迅速祸憋,開銷很少会宪。這一塊內(nèi)存地址稱為棧。棧是線程級(jí)別的蚯窥,大小在創(chuàng)建的時(shí)候已經(jīng)確定掸鹅,當(dāng)變量太大的時(shí)候塞帐,會(huì)"逃逸"到堆上,這種現(xiàn)象稱為內(nèi)存逃逸巍沙。

內(nèi)存逃逸的影響

通過前面講解的堆棧葵姥,我們知道堆分配昂貴,棧分配廉價(jià)句携,在go中所有內(nèi)存優(yōu)先棧分配榔幸。而堆是一塊沒有特定結(jié)構(gòu),也沒有固定大小的內(nèi)存區(qū)域矮嫉,可以根據(jù)需要進(jìn)行調(diào)整削咆。全局變量,內(nèi)存占用較大的局部變量敞临,函數(shù)調(diào)用結(jié)束后不能立刻回收的局部變量都會(huì)存在堆里面态辛。變量在堆上的分配和回收都比在棧上開銷大的多。對(duì)于 go 這種帶 GC 的語言來說挺尿,會(huì)增加 gc 壓力奏黑,同時(shí)也容易造成內(nèi)存碎片。

go中內(nèi)存逃逸的現(xiàn)象舉例

  • 局部變量x在函數(shù)結(jié)束后還被其他地方調(diào)用
func Foo()func(){
    x:=5
    return func(){
        x+=1
    }
}

func main(){
    foo:=Foo()
    foo()
}

我們使用go build -gcflags '-m -l' main.go 來查看內(nèi)存逃逸的情況编矾,

  • -m 會(huì)打印出逃逸分析的優(yōu)化策略熟史,實(shí)際上最多總共可以用 4 個(gè) -m,但是信息量較大窄俏,一般用 1 個(gè)就可以了
  • -l 會(huì)禁用函數(shù)內(nèi)聯(lián)蹂匹,在這里禁用掉 inline 能更好的觀察逃逸情況,減少干擾凹蜈。

或者通過反編譯命令go tool compile -S main.go 更底層限寞,更硬核,更準(zhǔn)確的方式來判斷一個(gè)對(duì)象是否逃逸

[圖片上傳失敗...(image-629c4-1683545907588)]
其中move to heap 是在代碼生成階段發(fā)生的仰坦,它是編譯器根據(jù)逃逸分析的結(jié)果履植,為變量生成在堆上分配內(nèi)存的代碼。escapes to heap 是在逃逸分析階段發(fā)生的悄晃,它是編譯器判斷一個(gè)變量是否需要在堆上分配內(nèi)存的過程玫霎。

  • 像下面這種指針類型的值,都會(huì)被存儲(chǔ)到堆上面妈橄,因?yàn)槭侵羔橆愋褪幾g器不知道在函數(shù)運(yùn)行結(jié)束后,外部還是否會(huì)用到它眷蚓,所以不能對(duì)它進(jìn)行回收鼻种,它就會(huì)認(rèn)為這個(gè)變量逃逸了,就會(huì)在堆上分配內(nèi)存溪椎。這樣可以保證變量在函數(shù)返回后仍然有效普舆,不會(huì)被椞窨冢回收校读。
    [圖片上傳失敗...(image-9c27df-1683545907588)]
    同理沼侣,下面這種也是內(nèi)存逃逸,因?yàn)榍衅瑂1指向的是底層數(shù)組歉秫,沒有發(fā)生逃逸蛾洛,切片里面元素x發(fā)生了逃逸
    [圖片上傳失敗...(image-86c4be-1683545907588)]
  • 還有一種情況,就是數(shù)據(jù)量太大雁芙,棧放不下了轧膘。也會(huì)發(fā)生逃逸,比如下面這個(gè)切片兔甘,大小是10000 * 8 = 80000字節(jié) = 80KB谎碍,而切片的預(yù)估容量是64k,所以發(fā)生內(nèi)存逃逸洞焙。當(dāng)然蟆淀,在32位操作系統(tǒng)中,超過32k就會(huì)發(fā)生內(nèi)存逃逸澡匪。
    [圖片上傳失敗...(image-3b5e7a-1683545907588)]
  • 還有像 interface 類型上調(diào)用方法熔任,接口在編譯的時(shí)候不知道foofunc怎么實(shí)現(xiàn)的。只有運(yùn)行的時(shí)候才知道唁情,所以interface變量使用堆分配疑苔。
    [圖片上傳失敗...(image-5f33c5-1683545907588)]

如何避免內(nèi)存逃逸

  • 盡量減少外部指針引用,必要的時(shí)候可以使用值傳遞甸鸟;
  • 對(duì)于自己定義的數(shù)據(jù)大小惦费,有一個(gè)基本的預(yù)判,盡量不要出現(xiàn)椙谰拢空間溢出的情況薪贫;
  • Golang中的接口類型的方法調(diào)用是動(dòng)態(tài)調(diào)度,如果對(duì)于性能要求比較高且訪問頻次比較高的函數(shù)調(diào)用篮绰,應(yīng)該盡量避免使用接口類型后雷;
  • 盡量不要寫閉包函數(shù),可讀性差且發(fā)生逃逸吠各。

總結(jié)

  • 逃逸分析在編譯階段確定哪些變量可以分配在棧中臀突,哪些變量分配在堆上
  • 逃逸分析減輕了GC壓力,提高程序的運(yùn)行速度
  • 棧上內(nèi)存使用完畢不需要GC處理贾漏,堆上內(nèi)存使用完畢會(huì)交給GC處理
  • 函數(shù)傳參時(shí)對(duì)于需要修改原對(duì)象值候学,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇傳指針纵散。對(duì)于只讀的占用內(nèi)存較小的結(jié)構(gòu)體梳码,直接傳值能夠獲得更好的性能
  • 根據(jù)代碼具體分析隐圾,盡量減少逃逸代碼,減輕GC壓力掰茶,提高性能
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暇藏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子濒蒋,更是在濱河造成了極大的恐慌盐碱,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪伙,死亡現(xiàn)場離奇詭異瓮顽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)围橡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門暖混,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翁授,你說我怎么就攤上這事拣播。” “怎么了黔漂?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵诫尽,是天一觀的道長。 經(jīng)常有香客問我炬守,道長牧嫉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任减途,我火速辦了婚禮酣藻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鳍置。我一直安慰自己辽剧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布税产。 她就那樣靜靜地躺著怕轿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辟拷。 梳的紋絲不亂的頭發(fā)上撞羽,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音衫冻,去河邊找鬼诀紊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛隅俘,可吹牛的內(nèi)容都是我干的邻奠。 我是一名探鬼主播笤喳,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼碌宴!你這毒婦竟也來了杀狡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤唧喉,失蹤者是張志新(化名)和其女友劉穎捣卤,沒想到半個(gè)月后忍抽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體八孝,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年鸠项,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了干跛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祟绊,死狀恐怖楼入,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情牧抽,我是刑警寧澤嘉熊,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扬舒,受9級(jí)特大地震影響阐肤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讲坎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一孕惜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晨炕,春花似錦衫画、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至费奸,卻和暖如春弥激,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背货邓。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工秆撮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人换况。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓职辨,卻偏偏與公主長得像盗蟆,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舒裤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • 原文:https://zhuanlan.zhihu.com/p/113643434[https://zhuanla...
    gykimo閱讀 767評(píng)論 0 0
  • 介紹 Go 語言較之 C 語言一個(gè)很大的優(yōu)勢就是自帶 GC 功能喳资,可 GC 并不是沒有代價(jià)的。寫 C 語言的時(shí)候腾供,...
    達(dá)菲格閱讀 4,507評(píng)論 0 14
  • 一仆邓、堆、棸楸睿基本概念 Go 有兩個(gè)地方可以分配內(nèi)存:一個(gè)全局堆空間用來動(dòng)態(tài)分配內(nèi)存节值,另一個(gè)是每個(gè) goroutine...
    無昵稱啊閱讀 1,023評(píng)論 0 0
  • 參考資源一參考資源二參考資源三 對(duì)于手動(dòng)管理內(nèi)存的語言,比如 C/C++榜聂,調(diào)用著名的malloc和new函數(shù)可以在...
    簾外五更風(fēng)閱讀 1,081評(píng)論 0 0
  • Go是一門帶有垃圾回收的現(xiàn)代語言搞疗,它拋棄了傳統(tǒng)C/C++的開發(fā)者需要手動(dòng)管理內(nèi)存的方式,實(shí)現(xiàn)了內(nèi)存的主動(dòng)申請(qǐng)和釋放...
    機(jī)器鈴砍菜刀s閱讀 512評(píng)論 1 1