什麼是堆和棧,它們在哪兒?--堆疊
問題描述
程式語言書籍中經常解釋值型別被建立在棧上,引用型別被建立在堆上,但是並沒有本質上解釋這堆和棧是什麼。我僅有高階語言程式設計經驗,沒有看過對此更清晰的解釋。我的意思是我理解什麼是棧,但是它們到底是什麼,在哪兒呢(站在實際的計算機實體記憶體的角度上看)?
- 在通常情況下由作業系統(OS)和語言的執行時(runtime)控制嗎?
- 它們的作用範圍是什麼?
- 它們的大小由什麼決定?
- 哪個更快?
答案一
棧是為執行執行緒留出的記憶體空間。當函式被呼叫的時候,棧頂為區域性變數和一些 bookkeeping 資料預留塊。當函式執行完畢,塊就沒有用了,可能在下次的函式呼叫的時候再被使用。棧通常用後進先出(LIFO)的方式預留空間;因此最近的保留塊(reserved block)通常最先被釋放。這麼做可以使跟蹤堆疊變的簡單;從棧中釋放塊(free block)只不過是指標的偏移而已。
堆(heap)是為動態分配預留的記憶體空間。和棧不一樣,從堆上分配和重新分配塊沒有固定模式;你可以在任何時候分配和釋放它。這樣使得跟蹤哪部分堆已經被分配和被釋放變的異常複雜;有許多定製的堆分配策略用來為不同的使用模式下調整堆的效能。
每一個執行緒都有一個棧,但是每一個應用程式通常都只有一個堆(儘管為不同型別分配記憶體使用多個堆的情況也是有的)。
直接回答你的問題: 1. 當執行緒建立的時候,作業系統(OS)為每一個系統級(system-level)的執行緒分配棧。通常情況下,作業系統通過呼叫語言的執行時(runtime)去為應用程式分配堆。 2. 棧附屬於執行緒,因此當執行緒結束時棧被回收。堆通常通過執行時在應用程式啟動時被分配,當應用程式(程序)退出時被回收。 3. 當執行緒被建立的時候,設定棧的大小。在應用程式啟動的時候,設定堆的大小,但是可以在需要的時候擴充套件(分配器向作業系統申請更多的記憶體)。 4. 棧比堆要快,因為它存取模式使它可以輕鬆的分配和重新分配記憶體(指標/整型只是進行簡單的遞增或者遞減運算),然而堆在分配和釋放的時候有更多的複雜的 bookkeeping 參與。另外,在棧上的每個位元組頻繁的被複用也就意味著它可能對映到處理器快取中,所以很快(譯者注:區域性性原理)。
答案二
Stack:
- 和堆一樣儲存在計算機 RAM 中。
- 在棧上建立變數的時候會擴充套件,並且會自動回收。
- 相比堆而言在棧上分配要快的多。
- 用資料結構中的棧實現。
- 儲存區域性資料,返回地址,用做引數傳遞。
- 當用棧過多時可導致棧溢位(無窮次(大量的)的遞迴呼叫,或者大量的記憶體分配)。
- 在棧上的資料可以直接訪問(不是非要使用指標訪問)。
- 如果你在編譯之前精確的知道你需要分配資料的大小並且不是太大的時候,可以使用棧。
- 當你程式啟動時決定棧的容量上限。
Heap:
- 和棧一樣儲存在計算機RAM。
- 在堆上的變數必須要手動釋放,不存在作用域的問題。資料可用 delete, delete[] 或者 free 來釋放。
- 相比在棧上分配記憶體要慢。
- 通過程式按需分配。
- 大量的分配和釋放可造成記憶體碎片。
- 在 C++ 中,在堆上建立數的據使用指標訪問,用 new 或者 malloc 分配記憶體。
- 如果申請的緩衝區過大的話,可能申請失敗。
- 在執行期間你不知道會需要多大的資料或者你需要分配大量的記憶體的時候,建議你使用堆。
- 可能造成記憶體洩露。
舉例:
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;
答案三
堆和棧是兩種記憶體分配的兩個統稱。可能有很多種不同的實現方式,但是實現要符合幾個基本的概念:
1.對棧而言,棧中的新加資料項放在其他資料的頂部,移除時你也只能移除最頂部的資料(不能越位獲取)。
2.對堆而言,資料項位置沒有固定的順序。你可以以任何順序插入和刪除,因為他們沒有“頂部”資料這一概念。
上面上個圖片很好的描述了堆和棧分配記憶體的方式。
在通常情況下由作業系統(OS)和語言的執行時(runtime)控制嗎?
如前所述,堆和棧是一個統稱,可以有很多的實現方式。計算機程式通常有一個棧叫做呼叫棧,用來儲存當前函式呼叫相關的資訊(比如:主調函式的地址,區域性變數),因為函式呼叫之後需要返回給主調函式。棧通過擴充套件和收縮來承載資訊。實際上,程式不是由執行時來控制的,它由程式語言、作業系統甚至是系統架構來決定。
堆是在任何記憶體中動態和隨機分配的(記憶體的)統稱;也就是無序的。記憶體通常由作業系統分配,通過應用程式呼叫 API 介面去實現分配。在管理動態分配記憶體上會有一些額外的開銷,不過這由作業系統來處理。
它們的作用範圍是什麼?
呼叫棧是一個低層次的概念,就程式而言,它和“作用範圍”沒什麼關係。如果你反彙編一些程式碼,你就會看到指標引用堆疊部分。就高階語言而言,語言有它自己的範圍規則。一旦函式返回,函式中的區域性變數會直接直接釋放。你的程式語言就是依據這個工作的。
在堆中,也很難去定義。作用範圍是由作業系統限定的,但是你的程式語言可能增加它自己的一些規則,去限定堆在應用程式中的範圍。體系架構和作業系統是使用虛擬地址的,然後由處理器翻譯到實際的實體地址中,還有頁面錯誤等等。它們記錄那個頁面屬於那個應用程式。不過你不用關心這些,因為你僅僅在你的程式語言中分配和釋放記憶體,和一些錯誤檢查(出現分配失敗和釋放失敗的原因)。
它們的大小由什麼決定?
依舊,依賴於語言,編譯器,作業系統和架構。棧通常提前分配好了,因為棧必須是連續的記憶體塊。語言的編譯器或者作業系統決定它的大小。不要在棧上儲存大塊資料,這樣可以保證有足夠的空間不會溢位,除非出現了無限遞迴的情況(額,棧溢位了)或者其它不常見了程式設計決議。
堆是任何可以動態分配的記憶體的統稱。這要看你怎麼看待它了,它的大小是變動的。在現代處理器中和作業系統的工作方式是高度抽象的,因此你在正常情況下不需要擔心它實際的大小,除非你必須要使用你還沒有分配的記憶體或者已經釋放了的記憶體。
哪個更快一些?
棧更快因為所有的空閒記憶體都是連續的,因此不需要對空閒記憶體塊通過列表來維護。只是一個簡單的指向當前棧頂的指標。編譯器通常用一個專門的、快速的暫存器來實現。更重要的一點事是,隨後的棧上操作通常集中在一個記憶體塊的附近,這樣的話有利於處理器的高速訪問(譯者注:區域性性原理)。
答案四
你問題的答案是依賴於實現的,根據不同的編譯器和處理器架構而不同。下面簡單的解釋一下:
- 棧和堆都是用來從底層作業系統中獲取記憶體的。
- 在多執行緒環境下每一個執行緒都可以有他自己完全的獨立的棧,但是他們共享堆。並行存取被堆控制而不是棧。
堆:
- 堆包含一個連結串列來維護已用和空閒的記憶體塊。在堆上新分配(用 new 或者 malloc)記憶體是從空閒的記憶體塊中找到一些滿足要求的合適塊。這個操作會更新堆中的塊連結串列。這些元資訊也儲存在堆上,經常在每個塊的頭部一個很小區域。
- 堆的增加新快通常從地地址向高地址擴充套件。因此你可以認為堆隨著記憶體分配而不斷的增加大小。如果申請的記憶體大小很小的話,通常從底層作業系統中得到比申請大小要多的記憶體。
- 申請和釋放許多小的塊可能會產生如下狀態:在已用塊之間存在很多小的空閒塊。進而申請大塊記憶體失敗,雖然空閒塊的總和足夠,但是空閒的小塊是零散的,不能滿足申請的大小,。這叫做“堆碎片”。
- 當旁邊有空閒塊的已用塊被釋放時,新的空閒塊可能會與相鄰的空閒塊合併為一個大的空閒塊,這樣可以有效的減少“堆碎片”的產生。
棧:
- 棧經常與 sp 暫存器(譯者注:"stack pointer",瞭解彙編的朋友應該都知道)一起工作,最初 sp 指向棧頂(棧的高地址)。
- CPU 用 push 指令來將資料壓棧,用 pop 指令來彈棧。當用 push 壓棧時,sp 值減少(向低地址擴充套件)。當用 pop 彈棧時,sp 值增大。儲存和獲取資料都是 CPU 暫存器的值。
- 當函式被呼叫時,CPU使用特定的指令把當前的 IP (譯者注:“instruction pointer”,是一個暫存器,用來記錄 CPU 指令的位置)壓棧。即執行程式碼的地址。CPU 接下來將呼叫函式地址賦給 IP ,進行呼叫。當函式返回時,舊的 IP 被彈棧,CPU 繼續去函式呼叫之前的程式碼。
- 當進入函式時,sp 向下擴充套件,擴充套件到確保為函式的區域性變數留足夠大小的空間。如果函式中有一個 32-bit 的區域性變數會在棧中留夠四位元組的空間。當函式返回時,sp 通過返回原來的位置來釋放空間。
- 如果函式有引數的話,在函式呼叫之前,會將引數壓棧。函式中的程式碼通過 sp 的當前位置來定位引數並訪問它們。
- 函式巢狀呼叫和使用魔法一樣,每一次新呼叫的函式都會分配函式引數,返回值地址、區域性變數空間、巢狀呼叫的活動記錄都要被壓入棧中。函式返回時,按照正確方式的撤銷。
- 棧要受到記憶體塊的限制,不斷的函式巢狀/為區域性變數分配太多的空間,可能會導致棧溢位。當棧中的記憶體區域都已經被使用完之後繼續向下寫(低地址),會觸發一個 CPU 異常。這個異常接下會通過語言的執行時轉成各種型別的棧溢位異常。(譯者注:“不同語言的異常提示不同,因此通過語言執行時來轉換”我想他表達的是這個含義)
*函式的分配可以用堆來代替棧嗎?
不可以的,函式的活動記錄(即區域性或者自動變數)被分配在棧上, 這樣做不但儲存了這些變數,而且可以用來巢狀函式的追蹤。
堆的管理依賴於執行時環境,C 使用 malloc ,C++ 使用 new ,但是很多語言有垃圾回收機制。
棧是更低層次的特性與處理器架構緊密的結合到一起。當堆不夠時可以擴充套件空間,這不難做到,因為可以有庫函式可以呼叫。但是,擴充套件棧通常來說是不可能的,因為在棧溢位的時候,執行執行緒就被作業系統關閉了,這已經太晚了。
譯者注
關於堆疊的這個帖子,對我來說,收穫非常多。我之前看過一些資料,自己寫程式碼的時候也常常思考。就這方面,也和祥子(我的大學舍友,現在北京郵電讀研,技術牛人)探討過多次了。但是終究是一個一個的知識點,這個帖子看完之後,豁然開朗,把知識點終於連線成了一個網。這種感覺,經歷過的一定懂得,期間的興奮不言而喻。
這個帖子跟帖者不少,我選了評分最高的四個。這四個之間也有一些是重複的觀點。個人鍾愛第四個回答者,我看的時候,瞬間高潮了,有木有?不過需要一些組合語言、作業系統、計算機組成原理的的基礎,知道那幾個暫存器是幹什麼的,要知道計算機的流水線指令工作機制,保護/恢復現場等概念。三個回覆者都涉及到了作業系統中虛擬記憶體;在比較速度的時候,大家一定要在腦中對“區域性性原理”和計算機快取記憶體有一個概念。
如果你把這篇文章看懂了,我相信你收穫的不只是堆和棧,你會理解的更多!
興奮之餘,有幾點還是要強調的,翻譯沒有逐字逐詞翻譯,大部分是通過我個人的知識積累和對回帖者的意圖揣測而來的。請大家不要咬文嚼字,逐個推敲,我們的目的在於技術交流,不是麼?達到這一目的就夠了。
下面是一些不確定點:
- 我沒有聽過 bookkeeping data 這種說法,故沒有翻譯。從上下文理解來看,可以想成是用來暫存器值?函式引數?返回地址?如果有了解具體含義的朋友,煩請告知。
- 棧和堆疊是一回事,英文表達是 stack,堆是 heap。
- 呼叫棧的概念,我是第一次聽說,不太熟悉。大家可以去查查資料研究一下。
以上,送給大家,本文結束。