Go內(nèi)存分配和逃逸分析-實(shí)踐總結(jié)篇
前言
大家好,我是陽(yáng)哥。
上期內(nèi)容給大家分享了:??Go內(nèi)存分配和逃逸分析-理論篇??
這期內(nèi)容帶大家理論聯(lián)系實(shí)踐,詳解Go逃逸分析的場(chǎng)景,我還找到了一個(gè)非常好用的在線協(xié)作工具,大家可以直接在線調(diào)試:?https://1024code.com/codecubes/biY4K3l
效果如下:
要搞清楚GO的逃逸分析一定要先搞清楚內(nèi)存分配和堆棧:
內(nèi)存既可以分配到堆中,也可以分配到棧中。
GO語(yǔ)言是如何進(jìn)行內(nèi)存分配的呢?其設(shè)計(jì)初衷和實(shí)現(xiàn)原理是什么呢?
要搞清楚上面的問(wèn)題,我們先來(lái)聊一下內(nèi)存管理和堆、棧的知識(shí)點(diǎn):
內(nèi)存管理
內(nèi)存管理主要包括兩個(gè)動(dòng)作:分配與釋放。逃逸分析就是服務(wù)于內(nèi)存分配的,而內(nèi)存的釋放由GC負(fù)責(zé)。
棧
在Go語(yǔ)言中,棧的內(nèi)存是由編譯器自動(dòng)進(jìn)行分配和釋放的,棧區(qū)往往存儲(chǔ)著函數(shù)參數(shù)、局部變量和調(diào)用函數(shù)幀,它們隨著函數(shù)的創(chuàng)建而分配,隨著函數(shù)的退出而銷毀。
Go應(yīng)用程序運(yùn)行時(shí),每個(gè) goroutine 都維護(hù)著一個(gè)自己的棧區(qū),這個(gè)棧區(qū)只能自己使用不能被其他 goroutine 使用。棧是調(diào)用棧(call stack)的簡(jiǎn)稱。一個(gè)棧通常又包含了許多棧幀(stack frame),它描述的是函數(shù)之間的調(diào)用關(guān)系
堆
與棧不同的是,堆區(qū)的內(nèi)存一般由編譯器和工程師自己共同進(jìn)行管理分配,交給 Runtime GC 來(lái)釋放。在堆上分配時(shí),必須找到一塊足夠大的內(nèi)存來(lái)存放新的變量數(shù)據(jù)。后續(xù)釋放時(shí),垃圾回收器掃描堆空間尋找不再被使用的對(duì)象。
我們可以簡(jiǎn)單理解為:我們用GO語(yǔ)言開(kāi)發(fā)過(guò)程中,要考慮的內(nèi)存管理只是針對(duì)堆內(nèi)存而言的。
程序在運(yùn)行期間可以主動(dòng)從堆上申請(qǐng)內(nèi)存,這些內(nèi)存通過(guò)Go的內(nèi)存分配器分配,并由垃圾收集器回收。
為了方便大家理解,我們?cè)購(gòu)囊韵陆嵌葘?duì)比一下堆棧:
堆和棧的對(duì)比
加鎖
- 棧不需要加鎖:每個(gè)goroutine都獨(dú)享自己的??臻g,這就意味著棧上的內(nèi)存操作是不需要加鎖的。
- 堆有時(shí)需要加鎖:堆上的內(nèi)存,有時(shí)需要加鎖防止多線程沖突
延伸知識(shí)點(diǎn):為什么堆上的內(nèi)存有時(shí)需要加鎖?而不是一直需要加鎖呢?
因?yàn)镚o的內(nèi)存分配策略學(xué)習(xí)了TCMalloc的線程緩存思想,他為每個(gè)處理器分配了一個(gè)mcache,注意:從mcache分配內(nèi)存也是無(wú)鎖的。
關(guān)注我,后面帶大家詳解這部分知識(shí)點(diǎn)。
性能
- 棧內(nèi)存管理 性能好:棧上的內(nèi)存,它的分配與釋放非常高效的。簡(jiǎn)單地說(shuō),它只需要兩個(gè)CPU指令:一個(gè)是分配入棧,另外一個(gè)是棧內(nèi)釋放。只需要借助于棧相關(guān)寄存器即可完成。
- 堆內(nèi)存管理 性能差:對(duì)于程序堆上的內(nèi)存回收,還需要有標(biāo)記清除階段,例如Go采用的三色標(biāo)記法。
緩存策略
- 棧緩存性能更好
- 堆緩存性能較差
原因是:棧內(nèi)存能更好地利用CPU的緩存策略,因?yàn)闂?臻g相較于堆來(lái)說(shuō)是更連續(xù)的。
下面就介紹今天的重頭戲了:
逃逸分析
上面說(shuō)了這么多堆和棧的知識(shí)點(diǎn),目的是為了讓大家更好的理解逃逸分析。
正如上面講的,相比于把內(nèi)存分配到堆中,分配到棧中優(yōu)勢(shì)更明顯。
Go語(yǔ)言也是這么做的:Go編譯器會(huì)盡可能將變量分配到到棧上。
但是,在函數(shù)返回后無(wú)法證明變量未被引用,則該變量將被分配到堆上,該變量不隨函數(shù)棧的回收而回收。以此避免懸掛指針(dangling pointer)的問(wèn)題。
另外,如果局部變量占用內(nèi)存非常大,也會(huì)將其分配在堆上。
Go是如何確定內(nèi)存是分配到棧上還是堆上的呢?
答案就是:逃逸分析。
編譯器通過(guò)逃逸分析技術(shù)去選擇堆或者棧,逃逸分析的基本思想如下:檢查變量的生命周期是否是完全可知的,如果通過(guò)檢查,則在棧上分配。否則,就是所謂的逃逸,必須在堆上進(jìn)行分配。
逃逸分析原則
Go語(yǔ)言雖然沒(méi)有明確說(shuō)明逃逸分析原則,但是有以下幾點(diǎn)準(zhǔn)則,是可以參考的。
- 不同于JAVA JVM的運(yùn)行時(shí)逃逸分析,Go的逃逸分析是在編譯期完成的:編譯期無(wú)法確定的參數(shù)類型必定放到堆中;
- 如果變量在函數(shù)外部存在引用,則必定放在堆中;
- 如果變量占用內(nèi)存較大時(shí),則優(yōu)先放到堆中;
- 如果變量在函數(shù)外部沒(méi)有引用,則優(yōu)先放到棧中;
逃逸分析舉例
我們使用這個(gè)命令來(lái)查看逃逸分析的結(jié)果:go build -gcflags '-m -m -l'
1.參數(shù)是interface類型
運(yùn)行結(jié)果
原因分析
因?yàn)镻rintln(a ...interface{})的參數(shù)是interface{}類型,編譯期無(wú)法確定其具體的參數(shù)類型,所以內(nèi)存分配到堆中。
2. 變量在函數(shù)外部有引用
運(yùn)行結(jié)果
原因分析
變量a在函數(shù)外部存在引用。
我們來(lái)分析一下執(zhí)行過(guò)程:當(dāng)函數(shù)執(zhí)行完畢,對(duì)應(yīng)的棧幀就被銷毀,但是引用已經(jīng)被返回到函數(shù)之外。如果這時(shí)外部通過(guò)引用地址取值,雖然地址還在,但是這塊內(nèi)存已經(jīng)被釋放回收了,這就是非法內(nèi)存。
為了避免上述非法內(nèi)存的情況,在這種情況下變量的內(nèi)存分配必須分配到堆上。
3. 變量?jī)?nèi)存占用較大
運(yùn)行結(jié)果
原因分析
我們定義了一個(gè)容量為10000的int類型切片,發(fā)生了逃逸,內(nèi)存分配到了堆上(heap)。
注意看:
我們?cè)俸?jiǎn)單修改一下代碼,將切片的容量和長(zhǎng)度修改為1,再次查看逃逸分析的結(jié)果,我們發(fā)現(xiàn),沒(méi)有發(fā)生逃逸,內(nèi)存默認(rèn)分類到了棧上。
所以,當(dāng)變量占用內(nèi)存較大時(shí),會(huì)發(fā)生逃逸分析,將內(nèi)存分配到堆上。
4. 變量大小不確定時(shí)
我們?cè)俸?jiǎn)單修改一下上面的代碼:
運(yùn)行結(jié)果
原因分析
我們通過(guò)控制臺(tái)的輸出結(jié)果可以很明顯的看出:發(fā)生了逃逸,分配到了heap堆中。
原因是這樣的:
我們雖然在代碼段中給變量 l 賦值了1,但是編譯期間只能識(shí)別到初始化int類型切片時(shí),傳入的長(zhǎng)度和容量是變量l,編譯期并不能確定變量l的值,所以發(fā)生了逃逸,會(huì)把內(nèi)存分配到堆中。
思考題
好了,我們舉了4個(gè)逃逸分析的經(jīng)典案例,相信聰明的你已經(jīng)理解了逃逸分析的作用和發(fā)生逃逸的場(chǎng)景。
我們來(lái)想一下,在理解逃逸分析的原理之后,在開(kāi)發(fā)的過(guò)程中如何更好的編碼,進(jìn)而提高程序的效率,更好的利用內(nèi)存呢?
如何實(shí)踐?
理解逃逸分析一定能幫助我們寫(xiě)出更好的程序。知道變量分配在棧堆之上的差別后,我們就要盡量寫(xiě)出分配在棧上的代碼。因?yàn)槎焉系淖兞孔兩俸?,可以減輕內(nèi)存分配的開(kāi)銷,減小GC的壓力,提高程序的運(yùn)行速度。
但是我們也要有過(guò)猶不及的指導(dǎo)思想。
我認(rèn)為沒(méi)有一成不變的開(kāi)發(fā)模式,我們一定是在不斷的需求變化,業(yè)務(wù)變化中求得平衡的:
舉個(gè)栗子
舉個(gè)日常開(kāi)發(fā)中函數(shù)傳參例子:
有些場(chǎng)景下我們不應(yīng)該傳遞結(jié)構(gòu)體指針,而應(yīng)該直接傳遞結(jié)構(gòu)體。
為什么會(huì)這樣呢?雖然直接傳遞結(jié)構(gòu)體需要值拷貝,但是這是在棧上完成的操作,開(kāi)銷遠(yuǎn)比變量逃逸后動(dòng)態(tài)地在堆上分配內(nèi)存少的多。
當(dāng)然這種做法不是絕對(duì)的,要根據(jù)場(chǎng)景去分析:
- 如果結(jié)構(gòu)體較大,傳遞結(jié)構(gòu)體指針更合適,因?yàn)橹羔橆愋拖啾戎殿愋湍芄?jié)省大量的內(nèi)存空間
- 如果結(jié)構(gòu)體較小,傳遞結(jié)構(gòu)體更適合,因?yàn)樵跅I戏峙鋬?nèi)存,可以有效減少GC壓力
總結(jié)
通過(guò)本文的介紹,相信你一定加深了堆棧的理解;搞清楚逃逸分析的作用和原理之后能夠指導(dǎo)我們寫(xiě)出更優(yōu)雅的代碼。
我們?cè)谌粘i_(kāi)發(fā)中,要根據(jù)實(shí)際場(chǎng)景考慮,如何將內(nèi)存盡量分配到棧中,減少GC的壓力,提高性能。
如何找到應(yīng)用開(kāi)發(fā)效率、程序運(yùn)行效率、對(duì)機(jī)器的壓力及負(fù)載的平衡點(diǎn),是程序員進(jìn)階之旅中的必修課。
本文轉(zhuǎn)載自微信公眾號(hào)「 程序員升級(jí)打怪之旅」,作者「王中陽(yáng)Go」,可以通過(guò)以下二維碼關(guān)注。
轉(zhuǎn)載本文請(qǐng)聯(lián)系「 程序員升級(jí)打怪之旅」公眾號(hào)。