問(wèn)題描述
??編程語(yǔ)言書(shū)籍中經(jīng)常解釋值類型被創(chuàng)建在棧上,引用類型被創(chuàng)建在堆上故俐,但是并沒(méi)有本質(zhì)上解釋這堆和棧是什么想鹰。我僅有高級(jí)語(yǔ)言編程經(jīng)驗(yàn),沒(méi)有看過(guò)對(duì)此更清晰的解釋购披。我的意思是我理解什么是棧杖挣,但是它們到底是什么,在哪兒呢(站在實(shí)際的計(jì)算機(jī)物理內(nèi)存的角度上看)刚陡?
在通常情況下由操作系統(tǒng)(OS)和語(yǔ)言的運(yùn)行時(shí)(runtime)控制嗎惩妇?
它們的作用范圍是什么?
它們的大小由什么決定筐乳?
哪個(gè)更快歌殃?
答案一
??棧是為執(zhí)行線程留出的內(nèi)存空間。當(dāng)函數(shù)被調(diào)用的時(shí)候蝙云,棧頂為局部變量和一些 bookkeeping 數(shù)據(jù)預(yù)留塊氓皱。當(dāng)函數(shù)執(zhí)行完畢,塊就沒(méi)有用了勃刨,可能在下次的函數(shù)調(diào)用的時(shí)候再被使用波材。棧通常用后進(jìn)先出(LIFO)的方式預(yù)留空間;因此最近的保留塊(reserved block)通常最先被釋放身隐。這么做可以使跟蹤堆棧變的簡(jiǎn)單廷区;從棧中釋放塊(free block)只不過(guò)是指針的偏移而已。
??堆(heap)是為動(dòng)態(tài)分配預(yù)留的內(nèi)存空間贾铝。和棧不一樣隙轻,從堆上分配和重新分配塊沒(méi)有固定模式埠帕;你可以在任何時(shí)候分配和釋放它。這樣使得跟蹤哪部分堆已經(jīng)被分配和被釋放變的異常復(fù)雜玖绿;有許多定制的堆分配策略用來(lái)為不同的使用模式下調(diào)整堆的性能敛瓷。
??每一個(gè)線程都有一個(gè)棧,但是每一個(gè)應(yīng)用程序通常都只有一個(gè)堆(盡管為不同類型分配內(nèi)存使用多個(gè)堆的情況也是有的)斑匪。
??直接回答你的問(wèn)題: 1. 當(dāng)線程創(chuàng)建的時(shí)候呐籽,操作系統(tǒng)(OS)為每一個(gè)系統(tǒng)級(jí)(system-level)的線程分配棧。通常情況下蚀瘸,操作系統(tǒng)通過(guò)調(diào)用語(yǔ)言的運(yùn)行時(shí)(runtime)去為應(yīng)用程序分配堆绝淡。 2. 棧附屬于線程,因此當(dāng)線程結(jié)束時(shí)棧被回收苍姜。堆通常通過(guò)運(yùn)行時(shí)在應(yīng)用程序啟動(dòng)時(shí)被分配牢酵,當(dāng)應(yīng)用程序(進(jìn)程)退出時(shí)被回收。 3. 當(dāng)線程被創(chuàng)建的時(shí)候衙猪,設(shè)置棧的大小馍乙。在應(yīng)用程序啟動(dòng)的時(shí)候,設(shè)置堆的大小垫释,但是可以在需要的時(shí)候擴(kuò)展(分配器向操作系統(tǒng)申請(qǐng)更多的內(nèi)存)丝格。 4. 棧比堆要快,因?yàn)樗嫒∧J绞顾梢暂p松的分配和重新分配內(nèi)存(指針/整型只是進(jìn)行簡(jiǎn)單的遞增或者遞減運(yùn)算)棵譬,然而堆在分配和釋放的時(shí)候有更多的復(fù)雜的 bookkeeping 參與显蝌。另外,在棧上的每個(gè)字節(jié)頻繁的被復(fù)用也就意味著它可能映射到處理器緩存中订咸,所以很快(譯者注:局部性原理)曼尊。
答案二
Stack:
和堆一樣存儲(chǔ)在計(jì)算機(jī) RAM 中。
在棧上創(chuàng)建變量的時(shí)候會(huì)擴(kuò)展脏嚷,并且會(huì)自動(dòng)回收骆撇。
相比堆而言在棧上分配要快的多。
用數(shù)據(jù)結(jié)構(gòu)中的棧實(shí)現(xiàn)父叙。
存儲(chǔ)局部數(shù)據(jù)神郊,返回地址,用做參數(shù)傳遞趾唱。
當(dāng)用棧過(guò)多時(shí)可導(dǎo)致棧溢出(無(wú)窮次(大量的)的遞歸調(diào)用涌乳,或者大量的內(nèi)存分配)。
在棧上的數(shù)據(jù)可以直接訪問(wèn)(不是非要使用指針訪問(wèn))甜癞。
如果你在編譯之前精確的知道你需要分配數(shù)據(jù)的大小并且不是太大的時(shí)候夕晓,可以使用棧。
當(dāng)你程序啟動(dòng)時(shí)決定棧的容量上限带欢。
Heap:
和棧一樣存儲(chǔ)在計(jì)算機(jī)RAM运授。
在堆上的變量必須要手動(dòng)釋放,不存在作用域的問(wèn)題乔煞。數(shù)據(jù)可用 delete, delete[] 或者 free 來(lái)釋放吁朦。
相比在棧上分配內(nèi)存要慢。
通過(guò)程序按需分配渡贾。
大量的分配和釋放可造成內(nèi)存碎片逗宜。
在 C++ 中,在堆上創(chuàng)建數(shù)的據(jù)使用指針訪問(wèn)空骚,用 new 或者 malloc 分配內(nèi)存纺讲。
如果申請(qǐng)的緩沖區(qū)過(guò)大的話,可能申請(qǐng)失敗囤屹。
在運(yùn)行期間你不知道會(huì)需要多大的數(shù)據(jù)或者你需要分配大量的內(nèi)存的時(shí)候熬甚,建議你使用堆。
可能造成內(nèi)存泄露肋坚。
舉例:
int foo()
{
char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack).
bool b = true; // Allocated on the stack.
if(b)
{
//Create 500 bytes on the stack
char buffer[500];
//Create 500 bytes on the heap
pBuffer = new char[500];
}//<-- buffer is deallocated here, pBuffer is not
}//<--- oops there's a memory leak, I should have called delete[] pBuffer;
答案三
??堆和棧是兩種內(nèi)存分配的兩個(gè)統(tǒng)稱乡括。可能有很多種不同的實(shí)現(xiàn)方式智厌,但是實(shí)現(xiàn)要符合幾個(gè)基本的概念:
1.對(duì)棧而言诲泌,棧中的新加數(shù)據(jù)項(xiàng)放在其他數(shù)據(jù)的頂部,移除時(shí)你也只能移除最頂部的數(shù)據(jù)(不能越位獲认撑簟)敷扫。
2.對(duì)堆而言,數(shù)據(jù)項(xiàng)位置沒(méi)有固定的順序诚卸。你可以以任何順序插入和刪除葵第,因?yàn)樗麄儧](méi)有“頂部”數(shù)據(jù)這一概念。
上面上個(gè)圖片很好的描述了堆和棧分配內(nèi)存的方式合溺。
在通常情況下由操作系統(tǒng)(OS)和語(yǔ)言的運(yùn)行時(shí)(runtime)控制嗎羹幸?
??如前所述,堆和棧是一個(gè)統(tǒng)稱辫愉,可以有很多的實(shí)現(xiàn)方式栅受。計(jì)算機(jī)程序通常有一個(gè)棧叫做調(diào)用棧,用來(lái)存儲(chǔ)當(dāng)前函數(shù)調(diào)用相關(guān)的信息(比如:主調(diào)函數(shù)的地址恭朗,局部變量)屏镊,因?yàn)楹瘮?shù)調(diào)用之后需要返回給主調(diào)函數(shù)。棧通過(guò)擴(kuò)展和收縮來(lái)承載信息痰腮。實(shí)際上而芥,程序不是由運(yùn)行時(shí)來(lái)控制的,它由編程語(yǔ)言膀值、操作系統(tǒng)甚至是系統(tǒng)架構(gòu)來(lái)決定棍丐。
??堆是在任何內(nèi)存中動(dòng)態(tài)和隨機(jī)分配的(內(nèi)存的)統(tǒng)稱误辑;也就是無(wú)序的。內(nèi)存通常由操作系統(tǒng)分配歌逢,通過(guò)應(yīng)用程序調(diào)用 API 接口去實(shí)現(xiàn)分配巾钉。在管理動(dòng)態(tài)分配內(nèi)存上會(huì)有一些額外的開(kāi)銷,不過(guò)這由操作系統(tǒng)來(lái)處理秘案。
它們的作用范圍是什么砰苍?
??調(diào)用棧是一個(gè)低層次的概念,就程序而言阱高,它和“作用范圍”沒(méi)什么關(guān)系赚导。如果你反匯編一些代碼,你就會(huì)看到指針引用堆棧部分赤惊。就高級(jí)語(yǔ)言而言吼旧,語(yǔ)言有它自己的范圍規(guī)則。一旦函數(shù)返回未舟,函數(shù)中的局部變量會(huì)直接直接釋放黍少。你的編程語(yǔ)言就是依據(jù)這個(gè)工作的。
??在堆中处面,也很難去定義厂置。作用范圍是由操作系統(tǒng)限定的,但是你的編程語(yǔ)言可能增加它自己的一些規(guī)則魂角,去限定堆在應(yīng)用程序中的范圍昵济。體系架構(gòu)和操作系統(tǒng)是使用虛擬地址的,然后由處理器翻譯到實(shí)際的物理地址中野揪,還有頁(yè)面錯(cuò)誤等等访忿。它們記錄那個(gè)頁(yè)面屬于那個(gè)應(yīng)用程序。不過(guò)你不用關(guān)心這些斯稳,因?yàn)槟銉H僅在你的編程語(yǔ)言中分配和釋放內(nèi)存海铆,和一些錯(cuò)誤檢查(出現(xiàn)分配失敗和釋放失敗的原因)。
它們的大小由什么決定挣惰?
??依舊卧斟,依賴于語(yǔ)言,編譯器憎茂,操作系統(tǒng)和架構(gòu)珍语。棧通常提前分配好了,因?yàn)闂1仨毷沁B續(xù)的內(nèi)存塊竖幔。語(yǔ)言的編譯器或者操作系統(tǒng)決定它的大小板乙。不要在棧上存儲(chǔ)大塊數(shù)據(jù),這樣可以保證有足夠的空間不會(huì)溢出拳氢,除非出現(xiàn)了無(wú)限遞歸的情況(額募逞,棧溢出了)或者其它不常見(jiàn)了編程決議蛋铆。
??堆是任何可以動(dòng)態(tài)分配的內(nèi)存的統(tǒng)稱。這要看你怎么看待它了放接,它的大小是變動(dòng)的刺啦。在現(xiàn)代處理器中和操作系統(tǒng)的工作方式是高度抽象的,因此你在正常情況下不需要擔(dān)心它實(shí)際的大小透乾,除非你必須要使用你還沒(méi)有分配的內(nèi)存或者已經(jīng)釋放了的內(nèi)存。
哪個(gè)更快一些磕秤?
??棧更快因?yàn)樗械目臻e內(nèi)存都是連續(xù)的乳乌,因此不需要對(duì)空閑內(nèi)存塊通過(guò)列表來(lái)維護(hù)。只是一個(gè)簡(jiǎn)單的指向當(dāng)前棧頂?shù)闹羔樖信亍>幾g器通常用一個(gè)專門(mén)的汉操、快速的寄存器來(lái)實(shí)現(xiàn)。更重要的一點(diǎn)事是蒙兰,隨后的棧上操作通常集中在一個(gè)內(nèi)存塊的附近磷瘤,這樣的話有利于處理器的高速訪問(wèn)(譯者注:局部性原理)。
答案四
??你問(wèn)題的答案是依賴于實(shí)現(xiàn)的搜变,根據(jù)不同的編譯器和處理器架構(gòu)而不同采缚。下面簡(jiǎn)單的解釋一下:
??棧和堆都是用來(lái)從底層操作系統(tǒng)中獲取內(nèi)存的。
??在多線程環(huán)境下每一個(gè)線程都可以有他自己完全的獨(dú)立的棧挠他,但是他們共享堆扳抽。并行存取被堆控制而不是棧。
堆:
??堆包含一個(gè)鏈表來(lái)維護(hù)已用和空閑的內(nèi)存塊殖侵。在堆上新分配(用 new 或者 malloc)內(nèi)存是從空閑的內(nèi)存塊中找到一些滿足要求的合適塊贸呢。這個(gè)操作會(huì)更新堆中的塊鏈表。這些元信息也存儲(chǔ)在堆上拢军,經(jīng)常在每個(gè)塊的頭部一個(gè)很小區(qū)域楞陷。
堆的增加新快通常從地地址向高地址擴(kuò)展。因此你可以認(rèn)為堆隨著內(nèi)存分配而不斷的增加大小茉唉。如果申請(qǐng)的內(nèi)存大小很小的話固蛾,通常從底層操作系統(tǒng)中得到比申請(qǐng)大小要多的內(nèi)存。
??申請(qǐng)和釋放許多小的塊可能會(huì)產(chǎn)生如下?tīng)顟B(tài):在已用塊之間存在很多小的空閑塊度陆。進(jìn)而申請(qǐng)大塊內(nèi)存失敗魏铅,雖然空閑塊的總和足夠,但是空閑的小塊是零散的坚芜,不能滿足申請(qǐng)的大小览芳,。這叫做“堆碎片”鸿竖。
??當(dāng)旁邊有空閑塊的已用塊被釋放時(shí)沧竟,新的空閑塊可能會(huì)與相鄰的空閑塊合并為一個(gè)大的空閑塊铸敏,這樣可以有效的減少“堆碎片”的產(chǎn)生。
棧:
??棧經(jīng)常與 sp 寄存器(譯者注:”stack pointer”悟泵,了解匯編的朋友應(yīng)該都知道)一起工作杈笔,最初 sp 指向棧頂(棧的高地址)。
??CPU 用 push 指令來(lái)將數(shù)據(jù)壓棧糕非,用 pop 指令來(lái)彈棧蒙具。當(dāng)用 push 壓棧時(shí),sp 值減少(向低地址擴(kuò)展)朽肥。當(dāng)用 pop 彈棧時(shí)禁筏,sp 值增大。存儲(chǔ)和獲取數(shù)據(jù)都是 CPU 寄存器的值衡招。
??當(dāng)函數(shù)被調(diào)用時(shí)篱昔,CPU使用特定的指令把當(dāng)前的 IP (譯者注:“instruction pointer”,是一個(gè)寄存器始腾,用來(lái)記錄 CPU 指令的位置)壓棧州刽。即執(zhí)行代碼的地址。CPU 接下來(lái)將調(diào)用函數(shù)地址賦給 IP 浪箭,進(jìn)行調(diào)用穗椅。當(dāng)函數(shù)返回時(shí),舊的 IP 被彈棧奶栖,CPU 繼續(xù)去函數(shù)調(diào)用之前的代碼房待。
??當(dāng)進(jìn)入函數(shù)時(shí),sp 向下擴(kuò)展驼抹,擴(kuò)展到確保為函數(shù)的局部變量留足夠大小的空間桑孩。如果函數(shù)中有一個(gè) 32-bit 的局部變量會(huì)在棧中留夠四字節(jié)的空間。當(dāng)函數(shù)返回時(shí)框冀,sp 通過(guò)返回原來(lái)的位置來(lái)釋放空間流椒。
??如果函數(shù)有參數(shù)的話,在函數(shù)調(diào)用之前明也,會(huì)將參數(shù)壓棧宣虾。函數(shù)中的代碼通過(guò) sp 的當(dāng)前位置來(lái)定位參數(shù)并訪問(wèn)它們。
??函數(shù)嵌套調(diào)用和使用魔法一樣温数,每一次新調(diào)用的函數(shù)都會(huì)分配函數(shù)參數(shù)绣硝,返回值地址、局部變量空間撑刺、嵌套調(diào)用的活動(dòng)記錄都要被壓入棧中鹉胖。函數(shù)返回時(shí),按照正確方式的撤銷。
??棧要受到內(nèi)存塊的限制甫菠,不斷的函數(shù)嵌套/為局部變量分配太多的空間挠铲,可能會(huì)導(dǎo)致棧溢出。當(dāng)棧中的內(nèi)存區(qū)域都已經(jīng)被使用完之后繼續(xù)向下寫(xiě)(低地址)寂诱,會(huì)觸發(fā)一個(gè) CPU 異常拂苹。這個(gè)異常接下會(huì)通過(guò)語(yǔ)言的運(yùn)行時(shí)轉(zhuǎn)成各種類型的棧溢出異常。(譯者注:“不同語(yǔ)言的異常提示不同痰洒,因此通過(guò)語(yǔ)言運(yùn)行時(shí)來(lái)轉(zhuǎn)換”我想他表達(dá)的是這個(gè)含義)
*函數(shù)的分配可以用堆來(lái)代替棧嗎瓢棒?
??不可以的,函數(shù)的活動(dòng)記錄(即局部或者自動(dòng)變量)被分配在棧上丘喻, 這樣做不但存儲(chǔ)了這些變量脯宿,而且可以用來(lái)嵌套函數(shù)的追蹤。
??堆的管理依賴于運(yùn)行時(shí)環(huán)境仓犬,C 使用 malloc 嗅绰,C++ 使用 new 舍肠,但是很多語(yǔ)言有垃圾回收機(jī)制搀继。
??棧是更低層次的特性與處理器架構(gòu)緊密的結(jié)合到一起。當(dāng)堆不夠時(shí)可以擴(kuò)展空間翠语,這不難做到叽躯,因?yàn)榭梢杂袔?kù)函數(shù)可以調(diào)用。但是肌括,擴(kuò)展棧通常來(lái)說(shuō)是不可能的点骑,因?yàn)樵跅R绯龅臅r(shí)候,執(zhí)行線程就被操作系統(tǒng)關(guān)閉了谍夭,這已經(jīng)太晚了黑滴。