我們程序中的變量大多被分配在內(nèi)存的兩個(gè)區(qū)域:statck
和heap
。
01
stack和heap
首先讓我們一起來(lái)回顧一下進(jìn)程的內(nèi)存分配:
我們寫的程序代碼跑起來(lái)后,會(huì)是一個(gè)進(jìn)程;OS會(huì)給我們的進(jìn)程分配內(nèi)存;內(nèi)存結(jié)構(gòu)大致如下:
OS給一個(gè)進(jìn)程分配的內(nèi)存空間大致可以分為:代碼區(qū)
、全局?jǐn)?shù)據(jù)區(qū)
、棧(stack)
、堆(heap)
、環(huán)境變量區(qū)域
以及中間空白的緩沖區(qū)
六個(gè)部分。其中,數(shù)據(jù)的增長(zhǎng)路徑除棧(stack)
是由高到低之外,其余的均是由低到高(可看圖中數(shù)據(jù)箭頭)。
我們思考一下,為什么棧(stack)區(qū)
這么特殊和其他區(qū)域路徑相反?還有,進(jìn)程內(nèi)存中stack
和heap
和數(shù)據(jù)結(jié)構(gòu)中的stack
和heap
名字都相同,是有什么聯(lián)系嗎?請(qǐng)帶著問(wèn)題往下看:
stack
: 是由程序側(cè)通過(guò)系統(tǒng)調(diào)用
向操作系統(tǒng)申請(qǐng)的,由操作系統(tǒng)管理和釋放,不需要程序員手動(dòng)管理;一般用于存放線程和函數(shù)中產(chǎn)生的臨時(shí)變量。這塊區(qū)域的數(shù)據(jù)使用速度較快,不用手動(dòng)管理,省心省力。
heap
:是由程序側(cè)通過(guò)系統(tǒng)調(diào)用
向操作系統(tǒng)申請(qǐng)的,但是需要程序員自行管理的內(nèi)存區(qū)域,因?yàn)榇藚^(qū)域的定位是global variable
,用于存放全局的變量(雖然很多編程語(yǔ)言中不這么利用);程序員需要手動(dòng)或者通過(guò)GC及時(shí)free或者delete此內(nèi)存區(qū)域中的數(shù)據(jù),但是也要注意:如果頻繁的進(jìn)行刪除和添加,會(huì)導(dǎo)致內(nèi)存碎片。
我們?cè)賮?lái)看看數(shù)據(jù)結(jié)構(gòu)中的stack
和heap
;
stack
Heap
堆的定義:
完全二叉樹(shù)
每一個(gè)節(jié)點(diǎn)的值都必須大于等于(或小于等于)其子樹(shù)中每個(gè)節(jié)點(diǎn)的值
根節(jié)點(diǎn)是最大數(shù)的叫做“大頂堆”,根節(jié)點(diǎn)是最小數(shù)的叫做“小頂堆”。
堆heap
這種數(shù)據(jù)結(jié)構(gòu)經(jīng)常利用在“如何快速定位并獲取到Top N最熱門的xxx”,通常的做法如下圖:
一句話總結(jié):進(jìn)程內(nèi)存中的棧區(qū)(stack)
使用的數(shù)據(jù)結(jié)構(gòu)就是stack
,內(nèi)存中的heap
和數(shù)據(jù)結(jié)構(gòu)中的heap
則毫無(wú)關(guān)系。
看如下C代碼:
int main() {
int a = 3;
int b = 5;
ret = add(a, b);
printf('%d', ret);
reuturn 0;
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
以上代碼在棧區(qū)中的數(shù)據(jù)是這樣的:
還及得上文中提到的:“進(jìn)程內(nèi)存中只有棧區(qū)(stack)
數(shù)據(jù)是由高位向低位增長(zhǎng)的,其余的均為由低位向高位增長(zhǎng)嗎?”
棧區(qū)用的數(shù)據(jù)結(jié)構(gòu)是棧,函數(shù)變量的銷毀和返回順序用逆恰好符合stack先進(jìn)后出的特點(diǎn),我覺(jué)得這是棧區(qū)(stack)
逆序增長(zhǎng)很重要的一點(diǎn)。
但是,最根本的原因還是在于:歷史遺留問(wèn)題。請(qǐng)看下圖:
heap
區(qū)域大小都是動(dòng)態(tài)分配的,都有“不確定性”,很顯然,左圖發(fā)生堆棧重疊
更小,且更適合內(nèi)存的充分利用。02
Go變量的位置
我們?cè)趯慍、PHP、Java的時(shí)候,可以很容易的知道,所寫的變量所在的位置:帶new
、malloc
等字段的,那一定是在堆上分配了,至于后續(xù)GC怎么處理,有沒(méi)有引用繼續(xù)關(guān)聯(lián),堆有沒(méi)與釋放,程序是否存在內(nèi)存泄露…這都是后續(xù)處理的問(wèn)題了;變量的存儲(chǔ)位置是妥妥的堆上了。但是,在用Go的時(shí)候要注意,new
、make
等等關(guān)鍵字都不好使,Go變量的位置不是由寫程序的程序員來(lái)決定的,而是Go自行處理;所以可能你的變量是new
出來(lái)的,但是,最終也不一定分配到堆上,很可能是分配在棧上。
Go把變量的位置在哪兒這件事對(duì)程序員“隱藏”了,Go自行處理;因?yàn)镚o認(rèn)為:變量的存儲(chǔ)位置,會(huì)對(duì)程序的性能有一定影響,而Go是計(jì)劃打造對(duì)性能有極致要求的程序,因而自己管了。
Go是這么管的:
首先,棧stack
上的效率肯定是比堆要高的,這算是常識(shí);Go在編譯期會(huì)對(duì)每一個(gè)函數(shù)變量做判斷,如果不能夠判斷此函數(shù)中的變量在返回之后是否仍被引用到,就給把變量扔堆heap
上,否則,就扔棧stack
上。但是注意:如果變量非常大,還是會(huì)扔到堆heap
上。
我們是否有辦法知道我們寫的Go程序中變量的位置呢?
答案是有的,Go向開(kāi)發(fā)者提供了變量逃逸分析的工具
go build -gcflags '-m -l' main.go // 這里的main.go也可以是某個(gè)具體的二進(jìn)制應(yīng)用程序
下面對(duì)如下代碼進(jìn)行逃逸分析:
import (
'fmt'
)
func main(){
a:= 3
b := 5
ret := add(a, b)
fmt.Println(ret)
}
func add(x,y int)int {
sum := x + y
return sum
}
分析結(jié)果:
./main.go:11:16: main ... argument does not escape
./main.go:11:16: ret escapes to heap
聯(lián)系客服