什麼是堆棧和堆?

What and where are the stack and heap?
投票:7816回答:25

編程語言書籍解釋了值類型是在堆棧上創建的,而引用類型是在堆上創建的,而沒有說明這兩個是什麼。 我還沒有閱讀清楚的解釋。 我了解堆棧是什麼。 但,

  • 它們在哪里和在哪裡(物理上在真實計算機的內存中)?
  • 它們在多大程度上受操作系統或語言運行時的控制?
  • 他們的範圍是什麼?
  • 什麼決定了它們的大小?
  • 是什麼使速度更快?
tags:memory-management,stack,language-agnostic,heap,dynamic-memory-allocation
25回答
705
投票

(我將此答案從另一個或多或少是這個問題的重複的問題上移開了。)

您問題的答案是特定於實現的,並且可能會因編譯器和處理器體系結構而異。 但是,這是一個簡化的說明。

  • 堆棧和堆都是從底層操作系統分配的內存區域(通常是虛擬內存,按需映射到物理內存)。
  • 在多線程環境中,每個線程將具有其自己的完全獨立的堆棧,但它們將共享堆。 並發訪問必須在堆上進行控制,而在堆棧上則不可能。

  • 堆包含已用和可用塊的鏈接列表。 堆上的新分配(通過newmalloc )是通過從一個空閒塊中創建一個合適的塊來滿足的。 這需要更新堆上的塊列表。 有關堆上塊的元信息通常也存儲在堆上每個塊前面的小區域中。
  • 隨著堆的增長,通常將新塊從低地址分配到高地址。 因此,您可以將堆視為存儲塊的 ,隨著分配的內存,其大小會增加。 如果堆對於分配而言太小,則通常可以通過從底層操​​作系統獲取更多內存來增加大小。
  • 分配和釋放許多小塊可能會使堆處於這樣一種狀態,即在已用塊之間散佈著許多小空閒塊。 分配大塊的請求可能會失敗,因為即使空閒塊的組合大小可能足夠大,也沒有一個空閒塊足以滿足分配請求。 這稱為堆碎片
  • 當與空閒塊相鄰的已用塊被釋放時,新的空閒塊可以與相鄰的空閒塊合併以創建更大的空閒塊,從而有效地減少了堆的碎片。

堆

堆棧

  • 堆棧通常與CPU上一個名為堆棧指針的特殊寄存器緊密配合使用。 最初,堆棧指針指向堆棧的頂部(堆棧中的最高地址)。
  • CPU具有用於入堆棧並將其從堆棧彈出的特殊指令。 每次推送都會將值存儲在堆棧指針的當前位置,並減少堆棧指針。 pop檢索堆棧指針所指向的值,然後增加堆棧指針(不要因向堆棧中添加一個值會減少堆棧指針,而刪除一個值會增加它的事實而感到困惑。請記住,堆棧會增長為底端)。 存儲和檢索的值是CPU寄存器的值。
  • 調用函數時,CPU使用特殊指令來推送當前指令指針 ,即在堆棧上執行的代碼的地址。 然後,CPU通過將指令指針設置為所調用函數的地址來跳轉至該函數。 稍後,當函數返回時,舊的指令指針會從堆棧中彈出,並在調用函數後立即在代碼處恢復執行。
  • 輸入函數後,將減少堆棧指針以在堆棧上為本地(自動)變量分配更多空間。 如果函數具有一個局部32位變量,則在堆棧上預留​​4個字節。 當函數返回時,將堆棧指針移回以釋放分配的區域。
  • 如果函數具有參數,則在調用函數之前將它們壓入堆棧。 然後,函數中的代碼可以從當前堆棧指針向上瀏覽堆棧以找到這些值。
  • 嵌套函數調用的工作方式就像一種魅力。 每個新的調用將分配函數參數,局部變量的返回地址和空間,並且這些激活記錄可以堆疊用於嵌套調用,並在函數返回時以正確的方式展開。
  • 由於堆棧是有限的內存塊,因此您可能會通過調用過多的嵌套函數和/或為局部變量分配過多的空間而導致堆棧溢出 通常,用於堆棧的存儲區的設置方式是,在堆棧底部(最低地址)以下進行寫入操作將觸發CPU陷阱或異常。 然後,運行時可以捕獲這種異常情況,並將其轉換為某種堆棧溢出異常。

堆棧

可以在堆而不是堆棧上分配函數嗎?

不可以,功能(即本地或自動變量)的激活記錄分配在堆棧上,不僅用於存儲這些變量,而且還用於跟踪嵌套的函數調用。

堆的管理方式實際上取決於運行時環境。 C使用malloc ,C ++使用new ,但是許多其他語言具有垃圾回收。

但是,堆棧是與處理器體系結構緊密相關的更底層的功能。 在沒有足夠空間的情況下增長堆並不難,因為可以在處理堆的庫調用中實現堆。 但是,堆棧堆棧的增長通常是不可能的,因為只有在為時已晚時才發現堆棧溢出。 並關閉執行線程是唯一可行的選擇。


392
投票

在下面的C#代碼中

public void Method1()
{
    int i = 4;
    int y = 2;
    class1 cls1 = new class1();
}

這是管理內存的方式

堆棧上變量的圖片

僅需要持續函數調用的Local Variables就進入堆棧。 堆用於變量,這些變量的壽命我們並不是很早就知道,但是我們希望它們能持續一段時間。 在大多數語言中,至關重要的一點是,我們要在編譯時知道要將變量存儲在堆棧中的大小。

對象(隨著我們更新它們的大小而變化)會進入堆,因為我們在創建時不知道它們將持續多長時間。 在許多語言中,都是對垃圾堆進行垃圾收集,以查找不再具有任何引用的對象(例如cls1對象)。

在Java中,大多數對象直接進入堆。 在諸如C / C ++之類的語言中,當您不處理指針時,結構和類通常可以保留在堆棧中。

更多信息可以在這裡找到:

堆棧和堆內存分配之間的區別«timmurphy.org

和這裡:

在堆棧和堆上創建對象

本文是上述圖片的來源: 六個重要的.NET概念:堆棧,堆,值類型,引用類型,裝箱和拆箱-CodeProject

但請注意,其中可能包含一些錯誤。


185
投票

為了澄清, 此答案的信息不正確( 托馬斯在評論後將其答案固定,很酷:))。 其他答案只是避免解釋靜態分配的含義。 因此,我將在下面解釋三種主要的分配形式,以及它們通常與堆,堆棧和數據段的關係。 我還將在C / C ++和Python中顯示一些示例,以幫助人們理解。

“靜態”(又名靜態分配)變量未分配在堆棧上。 不要這麼假設-許多人這樣做只是因為“靜態”聽起來很像“堆棧”。 它們實際上既不存在於堆棧中,也不存在於堆中。 是所謂的數據段的一部分

但是,通常最好考慮“ 作用域 ”和“ 生存期 ”,而不是“堆棧”和“堆”。

範圍是指代碼的哪些部分可以訪問變量。 通常,我們認為本地範圍 (只能由當前函數訪問)與全局範圍 (可以在任何地方訪問)相比,儘管範圍會變得更加複雜。

生存期是指在程序執行期間何時分配和釋放變量。 通常,我們認為靜態分配 (變量將在程序的整個過程中持續存在,從而使它在多個函數調用之間存儲相同的信息非常有用)與自動分配 (變量僅在單個函數調用中持續存在,從而對於以下情況有用)存儲僅在函數運行期間使用的信息,完成後可以丟棄)與動態分配 (持續時間在運行時定義的變量,而不是像靜態或自動那樣的編譯時間)進行存儲。

儘管大多數編譯器和解釋器在使用堆棧,堆等方面都類似地實現了此行為,但是只要行為正確,編譯器有時可能會破壞這些約定。 例如,由於優化,即使大多數局部變量存在於堆棧中,局部變量也可能僅存在於寄存器中或被完全刪除。 正如一些評論中指出的那樣,您可以自由地實現甚至不使用堆棧或堆,而是使用其他一些存儲機制的編譯器(很少用,因為堆棧和堆非常適合)。

我將提供一些簡單的帶註釋的C代碼來說明所有這些。 最好的學習方法是在調試器下運行程序並觀察行為。 如果您喜歡閱讀python,請跳到答案的結尾:)

// Statically allocated in the data segment when the program/DLL is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in the code
int someGlobalVariable;

// Statically allocated in the data segment when the program is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in this particular code file
static int someStaticVariable;

// "someArgument" is allocated on the stack each time MyFunction is called
// "someArgument" is deallocated when MyFunction returns
// scope - can be accessed only within MyFunction()
void MyFunction(int someArgument) {

    // Statically allocated in the data segment when the program is first loaded
    // Deallocated when the program/DLL exits
    // scope - can be accessed only within MyFunction()
    static int someLocalStaticVariable;

    // Allocated on the stack each time MyFunction is called
    // Deallocated when MyFunction returns
    // scope - can be accessed only within MyFunction()
    int someLocalVariable;

    // A *pointer* is allocated on the stack each time MyFunction is called
    // This pointer is deallocated when MyFunction returns
    // scope - the pointer can be accessed only within MyFunction()
    int* someDynamicVariable;

    // This line causes space for an integer to be allocated in the heap
    // when this line is executed. Note this is not at the beginning of
    // the call to MyFunction(), like the automatic variables
    // scope - only code within MyFunction() can access this space
    // *through this particular variable*.
    // However, if you pass the address somewhere else, that code
    // can access it too
    someDynamicVariable = new int;


    // This line deallocates the space for the integer in the heap.
    // If we did not write it, the memory would be "leaked".
    // Note a fundamental difference between the stack and heap
    // the heap must be managed. The stack is managed for us.
    delete someDynamicVariable;

    // In other cases, instead of deallocating this heap space you
    // might store the address somewhere more permanent to use later.
    // Some languages even take care of deallocation for you... but
    // always it needs to be taken care of at runtime by some mechanism.

    // When the function returns, someArgument, someLocalVariable
    // and the pointer someDynamicVariable are deallocated.
    // The space pointed to by someDynamicVariable was already
    // deallocated prior to returning.
    return;
}

// Note that someGlobalVariable, someStaticVariable and
// someLocalStaticVariable continue to exist, and are not
// deallocated until the program exits.

為什麼區分生命週期和作用域很重要的一個特別令人毛骨悚然的示例是變量可以具有局部作用域,但具有靜態生命週期-例如,上面的代碼示例中的“ someLocalStaticVariable”。 這樣的變量會使我們常見但非正式的命名習慣非常混亂。 例如,當我們說“ 本地 ”時,我們通常是指“ 本地範圍內的自動分配的變量 ”,而當我們說“ 全局 ”時,我們通常是指“ 全局範圍的靜態分配的變量 ”。 不幸的是,當涉及到“ 文件範圍內的靜態分配的變量 ”之類的事情時,許多人只是說...“ 呵呵 ”。

C / C ++中的某些語法選擇加劇了這個問題-例如,由於以下所示的語法,許多人認為全局變量不是“靜態”的。

int var1; // Has global scope and static allocation
static int var2; // Has file scope and static allocation

int main() {return 0;}

請注意,在上面的聲明中添加關鍵字“ static”可防止var2具有全局作用域。 但是,全局var1具有靜態分配。 這不直觀! 因此,在描述範圍時,我盡量不要使用“靜態”一詞,而應使用諸如“文件”或“文件受限”範圍之類的名稱。 但是,許多人使用短語“靜態”或“靜態作用域”來描述只能從一個代碼文件訪問的變量。 在生命週期的上下文中,“靜態” 始終表示變量在程序啟動時分配,並在程序退出時釋放。

有些人認為這些概念是C / C ++特有的。 他們不是。 例如,下面的Python示例說明了所有三種分配類型(在解釋性語言中可能存在一些細微的差異,我將不在這裡介紹)。

from datetime import datetime

class Animal:
    _FavoriteFood = 'Undefined' # _FavoriteFood is statically allocated

    def PetAnimal(self):
        curTime = datetime.time(datetime.now()) # curTime is automatically allocatedion
        print("Thank you for petting me. But it's " + str(curTime) + ", you should feed me. My favorite food is " + self._FavoriteFood)

class Cat(Animal):
    _FavoriteFood = 'tuna' # Note since we override, Cat class has its own statically allocated _FavoriteFood variable, different from Animal's

class Dog(Animal):
    _FavoriteFood = 'steak' # Likewise, the Dog class gets its own static variable. Important to note - this one static variable is shared among all instances of Dog, hence it is not dynamic!


if __name__ == "__main__":
    whiskers = Cat() # Dynamically allocated
    fido = Dog() # Dynamically allocated
    rinTinTin = Dog() # Dynamically allocated

    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

    Dog._FavoriteFood = 'milkbones'
    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

# Output is:
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is milkbones
# Thank you for petting me. But it's 13:05:02.256000, you should feed me. My favorite food is milkbones

51
投票

  • 快速訪問
  • 不必顯式取消分配變量
  • CPU有效管理空間,內存不會碎片化
  • 僅局部變量
  • 堆棧大小限制(取決於OS)
  • 變量無法調整大小

  • 變量可以全局訪問
  • 內存大小無限制
  • (相對)訪問速度較慢
  • 無法保證有效利用空間,隨著時間的流逝,隨著分配內存塊然後釋放內存,內存可能會碎片化
  • 您必須管理內存(您負責分配和釋放變量)
  • 可以使用realloc()調整變量的大小

108
投票

什麼是堆棧?

堆棧是一堆物體,通常是整齊排列的物體。

在此處輸入圖片說明

計算體系結構中的堆棧是內存中以先進先出方式添加或刪除數據的區域。
在多線程應用程序中,每個線程將具有自己的堆棧。

什麼是堆?

堆是隨意堆放的東西的不整潔集合。

在此處輸入圖片說明

在計算體系結構中,堆是由操作系統或內存管理器庫自動管理的動態分配內存區域。
在程序執行期間定期對堆上的內存進行分配,釋放和調整大小,這可能導致稱為碎片的問題。
當在內存對象之間分配的小空間太小而無法容納其他內存對象時,就會發生碎片。
最終結果是無法用於進一步的內存分配的堆空間的百分比。

兩者一起

在多線程應用程序中,每個線程將具有自己的堆棧。 但是,所有不同的線程將共享堆。
因為不同的線程在多線程應用程序中共享堆,所以這也意味著線程之間必須進行某種協調,以便它們不會嘗試訪問和操作堆中相同的內存。同一時間。

哪個更快-堆棧還是堆? 又為什麼呢?

堆棧比堆快得多。
這是因為在堆棧上分配內存的方式。
在堆棧上分配內存就像向上移動堆棧指針一樣簡單。

對於剛接觸編程的人來說,使用堆棧可能是個好主意,因為它比較容易。
因為堆棧很小,所以當您確切知道數據將需要多少內存時,或者如果您知道數據的大小很小時,您可能希望使用它。
當您知道您的數據將需要大量內存,或者您不確定需要多少內存(例如動態數組)時,最好使用堆。

Java內存模型

在此處輸入圖片說明

堆棧是存儲本地變量(包括方法參數)的內存區域。 當涉及對像變量時,這些僅僅是對堆上實際對象的引用(指針)。
每次實例化一個對象時,都會將一大堆堆內存放在一邊以保存該對象的數據(狀態)。 由於對象可以包含其他對象,因此某些數據實際上可以保存對這些嵌套對象的引用。


21
投票

由於有些答案很挑剔,所以我將貢獻自己的一份力量。

令人驚訝的是,沒有人提到,不僅在異類語言(PostScript)或平台(Intel Itanium)中,而且在光纖綠色線程中都發現了多個(即與正在運行的OS級線程的數量無關)調用堆棧。和協程的一些實現。

纖維,生絲和協程在許多方面都相似,這導致很多混亂。 光纖和綠色線程之間的區別在於,前者使用協作式多任務處理,而後者則可以採用協作式或搶占式(或什至兩者)。 有關纖維和協程之間的區別,請參見此處

無論如何,光纖,綠色線程和協程的目的是在單個OS級線程中同時執行多個功能,但不能並行執行(請參見此SO問題以區分它們),從而相互之間來回傳遞控制以有組織的方式

使用纖維,綠線或協程時, 通常每個功能都有單獨的堆棧。 (從技術上講,每個函數不僅是堆棧,而且整個執行上下文是每個函數。最重要的是,CPU寄存器。)對於每個線程,堆棧與同時運行的函數一樣多,並且線程在執行每個函數之間切換根據您程序的邏輯。 當函數運行到末尾時,其堆棧將被破壞。 因此, 堆棧的數量和生存期是動態的, 而不是由OS級線程的數量決定的!

請注意,我說的是“每個函數通常有單獨的堆棧”。 還有就是couroutines兩個stackful無堆棧實現。 最著名的堆棧式C ++實現是Boost.CoroutineMicrosoft PPLasync/await (但是,C ++ 17提出的C ++ 可恢復函數 (又名“ async and await ”)可能會使用無堆棧協程。)

即將向C ++標準庫提出光纖建議。 另外,還有一些第三方 綠色線程在Python和Ruby等語言中非常流行。


35
投票

在1980年代,UNIX像小兔子一樣傳播,大公司也紛紛成立自己的公司。 埃克森美孚(Exxon)和一個失去歷史的品牌一樣擁有一個。 內存的佈局方式由許多實現者決定。

典型的C程序在內存中平面放置,可以通過更改brk()值來增加。 通常,HEAP剛好低於此brk值,並且增加brk會增加可用堆的數量。

單個STACK通常位於HEAP之下的區域,該區域是直到下一固定內存塊頂部之前都沒有值的內存區域。 下一個塊通常是CODE,在當時的著名黑客之一中,堆棧數據可能會覆蓋它。

一個典型的存儲塊是BSS(零值塊),它在一個製造商的產品中偶然​​沒有被置零。 另一個是包含初始化值的DATA,包括字符串和數字。 第三個是包含CRT(C運行時),主要,功能和庫的CODE。

UNIX中虛擬內存的出現改變了許多限制。 沒有客觀原因說明為什麼這些塊需要連續,大小固定或現在以特定方式訂購。 當然,在UNIX出現之前,Multics不受這些限制。 這是顯示那個時代的一種存儲器佈局的示意圖。

典型的1980年代風格的UNIX C程序內存佈局


24
投票

幾分錢:我認為,以圖形方式繪製內存並且更簡單會很好:

這是我對進程內存構造進行簡化的願景,以便更輕鬆地了解正在發生的事情


箭頭-通常在線程創建API中通過參數顯示增長堆棧和堆的位置,進程堆棧大小的限制(在OS中定義),線程堆棧大小的限制。 堆通常受進程最大虛擬內存大小的限制,例如32位2-4 GB。

如此簡單的方式:進程堆是進程及其內部所有線程的通用對象,通常在諸如malloc()之類的情況下用於內存分配。

堆棧是快速存儲器,用於在通常情況下存儲函數返回指針和變量,並作為函數調用中的參數(局部函數變量)進行處理。


43
投票

簡而言之

堆棧用於存儲靜態內存,堆用於存儲動態內存,兩者均存儲在計算機的RAM中。


詳細

堆棧

堆棧是一個“ LIFO”(後進先出)數據結構,該結構由CPU非常緊密地管理和優化。 每當函數聲明一個新變量時,它都會“推送”到堆棧中。 然後,每次函數退出時,該函數壓入堆棧的所有變量都將被釋放(也就是說,它們將被刪除)。 釋放堆棧變量後,該內存區域可用於其他堆棧變量。

使用堆棧存儲變量的優點是可以為您管理內存。 您不必手動分配內存,也可以在不再需要時釋放它。 此外,由於CPU如此高效地組織堆棧存儲器,因此讀寫堆棧變量的速度非常快。

這裡可以找到更多。


堆是計算機內存中不會自動為您管理的區域,並且不受CPU嚴格管理。 它是內存中更自由浮動的區域(並且更大)。 要在堆上分配內存,必須使用內置的C函數malloc()或calloc()。 在堆上分配內存後,一旦不再需要使用free()負責分配該內存。

如果不這樣做,程序將發生所謂的內存洩漏。 也就是說,堆上的內存仍將被保留(並且其他進程將無法使用)。 正如我們將在調試部分看到的那樣,有一個名為Valgrind的工具可以幫助您檢測內存洩漏。

與堆棧不同,堆對可變大小沒有大小限制(除了計算機的明顯物理限制之外)。 堆內存的讀取和寫入速度稍慢,因為必須使用指針來訪問堆上的內存。 我們將在短期內討論指針。

與堆棧不同,在堆上創建的變量可以由程序中任何位置的任何函數訪問。 堆變量的範圍本質上是全局的。

這裡可以找到更多。


在堆棧上分配的變量直接存儲到內存中,對該內存的訪問非常快,並且在編譯程序時會處理其分配。 當一個函數或方法調用另一個函數,然後又調用另一個函數等時,所有這些函數的執行將保持掛起狀態,直到最後一個函數返回其值為止。 堆棧始終按LIFO順序保留,最近保留的塊始終是要釋放的下一個塊。 這使得跟踪堆棧真的非常簡單,從堆棧中釋放一個塊只不過是調整一個指針而已。

在堆上分配的變量在運行時分配了內存,訪問該內存的速度稍慢,但是堆大小僅受虛擬內存大小的限制。 堆中的元素彼此之間沒有依賴關係,並且始終可以隨時隨地進行隨機訪問。 您可以隨時分配一個塊,並隨時釋放它。 這使跟踪在任何給定時間分配或釋放堆的哪些部分變得更加複雜。

在此處輸入圖片說明

如果您確切知道在編譯之前需要分配多少數據,並且它不會太大,則可以使用堆棧。 如果您不確切知道運行時將需要多少數據,或者是否需要分配大量數據,則可以使用堆。

在多線程情況下,每個線程將具有其自己的完全獨立的堆棧,但它們將共享堆。 堆棧是特定於線程的,堆是特定於應用程序的。 在異常處理和線程執行中,必須考慮堆棧。

每個線程都有一個堆棧,而應用程序通常只有一個堆(儘管對於不同類型的分配有多個堆並不少見)。

在此處輸入圖片說明

在運行時,如果應用程序需要更多的堆,則可以從可用內存中分配內存,如果堆棧需要內存,則可以從可用內存中為應用程序分配的內存中分配內存。

甚至在這里這裡都給出更多細節。


現在來回答您的問題

它們在多大程度上受操作系統或語言運行時的控制?

創建線程時,操作系統會為每個系統級線程分配堆棧。 通常,語言運行庫會調用OS來為應用程序分配堆。

這裡可以找到更多。

他們的範圍是什麼?

已經在頂部。

“如果知道在編譯之前需要分配多少數據,並且它不會太大,那麼可以使用堆棧。如果您不清楚在運行時需要多少數據,或者您需要分配大量數據。”

這裡可以找到更多。

什麼決定了它們的大小?

創建線程時,堆棧大小由OS設置。 堆的大小是在應用程序啟動時設置的,但是它可以隨著空間的增長而增長(分配器從操作系統請求更多的內存)。

是什麼使速度更快?

堆棧分配要快得多,因為它真正要做的只是移動堆棧指針。 使用內存池,您可以從堆分配中獲得可比的性能,但這會帶來一點點複雜性和麻煩。

而且,堆棧與堆不僅是性能方面的考慮; 它還告訴您很多有關對象的預期壽命的信息。

可以從這裡找到詳細信息。


44
投票

好吧,簡單地說,它們的意思是有序的不是有序的 ……!

堆放 :在堆放物品中,東西相互疊放,這意味著將更快,更高效地進行處理!...

因此總會有一個索引來指向特定的項目,並且處理也會更快,項目之間也有關係!...

:沒有順序,處理會變慢,並且值被弄亂了,沒有特定的順序或索引……是隨機的,它們之間沒有關係……所以執行和使用時間可能會有所不同……

我還創建了下面的圖像,以顯示它們的外觀:

在此處輸入圖片說明


8
投票

許多答案在概念上都是正確的,但是我們必須注意,硬件(即微處理器)需要一個堆棧才能允許調用子例程(彙編語言中的CALL ..)。 (OOP傢伙會稱其為方法

在堆棧上,您可以保存返回地址並調用→推入/退出→彈出窗口直接在硬件中管理。

您可以使用堆棧來傳遞參數..即使它比使用寄存器要慢(微處理器專家或一本好的1980年代BIOS書籍也會這樣)。

  • 沒有堆棧中沒有微處理器可以工作。 (我們無法想像沒有子例程/函數的程序,即使是彙編語言)
  • 沒有堆就可以。 (彙編語言程序可以不工作,因為堆是OS概念,而malloc則是OS / Lib調用。

堆棧使用速度更快,因為:

  • 是硬件,甚至push / pop都非常有效。
  • malloc需要進入內核模式,使用鎖/信號量(或其他同步原語)執行一些代碼並管理一些跟踪分配所需的結構。

32
投票

虛擬內存中每個進程的堆棧數據

堆棧,堆和靜態數據


15
投票

我已經分享了一些要點,儘管要點已經涵蓋了。

  • 快速訪問。
  • 存儲在RAM中。
  • 函數調用與傳遞的局部變量和函數參數一起加載到此處。
  • 當程序超出範圍時,將自動釋放空間。
  • 存儲在順序存儲器中。

  • 相對較慢地訪問堆棧。
  • 存儲在RAM中。
  • 動態創建的變量存儲在此處,以後需要使用後釋放分配的內存。
  • 存儲在完成內存分配的任何位置,始終由指針訪問。

有趣的注意事項:

  • 如果將函數調用存儲在堆中,則會導致2個混亂點:
    1. 由於堆棧中的順序存儲,因此執行速度更快。 堆中的存儲會導致大量時間消耗,從而使整個程序的執行速度變慢。
    2. 如果函數存儲在堆中(指針指向的混亂存儲),則將無法返回到調用者地址(由於內存中的順序存儲,所以堆棧可以返回)。

12
投票

哇! 如此眾多的答案,我認為其中沒有一個答案正確...

1)它們在哪里和什麼(物理上在真實計算機的內存中)?

堆棧是從分配給程序映像的最高內存地址開始的內存,然後從那裡遞減值。 它保留給調用的函數參數以及函數中使用的所有臨時變量。

有兩個堆:公共堆和私有堆。

私有堆從程序中代碼的最後一個字節之後的16字節邊界(對於64位程序)或8字節邊界(對於32位程序)開始,然後從那裡開始增加值。 也稱為默認堆。

如果私有堆太大,則它將與堆棧區域重疊,如果私有堆太大,則堆棧將與堆棧重疊。 由於堆棧從較高的地址開始,然後一直向下移動至較低的地址,因此通過適當的破解,您可以使堆棧變大,以至於溢出專用堆區域並重疊代碼區域。 然後,訣竅是要重疊足夠的代碼區域,以便可以將其連接到代碼中。 這樣做有些棘手,您可能會遇到程序崩潰的風險,但它既簡單又有效。

公共堆位於程序映像空間之外的自己的內存空間中。 如果內存資源稀缺,則會將該內存虹吸到硬盤上。

2)它們在多大程度上受操作系統或語言運行時的控制?

堆棧由程序員控制,私有堆由OS管理,而公共堆則不受任何人控制,因為它是OS服務-您發出請求,然後授予或拒絕請求。

2b)他們的範圍是什麼?

它們對於該程序都是全局的,但是其內容可以是私有的,公共的或全局的。

2c)是什麼決定了每個尺寸?

堆棧和專用堆的大小由編譯器運行時選項確定。 公共堆在運行時使用size參數初始化。

2d)是什麼使速度更快?

它們並不是為了快速而設計的,而是被設計為有用的。 程序員如何利用它們確定它們是“快速”還是“慢速”

參考:

https://norasandler.com/2019/02/18/Write-a-Compiler-10.html

https://docs.microsoft.com/zh-CN/windows/desktop/api/heapapi/nf-heapapi-getprocessheap

https://docs.microsoft.com/zh-CN/windows/desktop/api/heapapi/nf-heapapi-heapcreate


1339
投票

最重要的一點是,堆和堆棧是內存分配方式的通用術語。 它們可以以許多不同的方式實現,並且這些術語適用於基本概念。

  • 在一堆物品中,物品按照放置在另一個物品上的順序放在另一個物品的頂部,並且您只能刪除頂部的物品(不能將整個物品翻倒)。

    像紙堆一樣堆

    堆棧的簡單性在於,您不需要維護一個表,該表包含分配的內存的每個部分的記錄。 您唯一需要的狀態信息是指向堆棧末尾的單個指針。 要分配和取消分配,您只需遞增和遞減該單個指針。 注意:有時可以將堆棧實現為從內存部分的頂部開始,然後向下擴展而不是向上擴展。

  • 在堆中,項的放置方式沒有特定的順序。 因為沒有清晰的“頂部”項目,所以您可以按任何順序進入和移除項目。

    像一堆甘草一樣堆

    堆分配需要完整記錄分配的內存和未分配的內存,並進行一些開銷維護以減少碎片,找到足夠大以適合請求的大小的連續內存段,依此類推。 可以隨時釋放內存以留下可用空間。 有時,內存分配器將執行維護任務,例如通過移動分配的內存來對內存進行碎片整理或垃圾回收-在運行時識別內存不再在作用域中並對其進行分配。

這些映像應該在描述堆棧和堆中分配和釋放內存的兩種方式方面做得相當好。 好吃

  • 它們在多大程度上受操作系統或語言運行時的控制?

    如前所述,堆和堆棧是通用術語,可以通過多種方式實現。 計算機程序通常具有稱為調用堆棧的堆棧 ,該堆棧存儲與當前功能相關的信息,例如指向從哪個函數調用的指針以及任何局部變量。 由於函數先調用其他函數然後返回,所以堆棧會不斷擴大和縮小,以保存來自函數的信息,直到調用堆棧為止。 一個程序實際上並沒有對它的運行時控制。 它由編程語言,操作系統甚至系統架構決定。

    堆是用於動態且隨機分配的任何內存的通用術語。 即亂序。 內存通常由OS分配,應用程序調用API函數進行此分配。 管理動態分配的內存需要一定的開銷,通常由OS處理。

  • 他們的範圍是什麼?

    調用棧是一個低級概念,從編程的意義上講它與“作用域”無關。 如果您分解一些代碼,則會看到指向堆棧部分的相對指針樣式引用,但是就高級語言而言,該語言強加了自己的作用域規則。 但是,堆棧的一個重要方面是,一旦函數返回,該函數本地的所有內容都會立即從堆棧中釋放出來。 鑑於您的編程語言是如何工作的,這種工作方式與您期望的一樣。 在堆中,也很難定義。 範圍是操作系統公開的任何內容,但是您的編程語言可能會添加有關其在應用程序中的“作用域”的規則。 處理器體系結構和操作系統使用虛擬尋址,虛擬尋址將處理器轉換為物理地址,並且存在頁面錯誤等。它們跟踪哪些頁面屬於哪些應用程序。 但是,您實際上不必擔心這一點,因為您只需使用編程語言用來分配和釋放內存的任何方法,並檢查錯誤(如果分配/釋放由於任何原因而失敗)。

  • 什麼決定了它們的大小?

    同樣,它取決於語言,編譯器,操作系統和體系結構。 堆棧通常是預先分配的,因為根據定義,它必須是連續的內存(有關最後一段的更多信息)。 語言編譯器或操作系統確定其大小。 您不會在堆棧上存儲大量數據,因此它將足夠大,以至於永遠不要完全使用它,除非發生不必要的無窮遞歸(因此,“堆棧溢出”)或其他異常的編程決策。

    堆是可以動態分配的任何事物的通用術語。 根據您看待它的方式,它會不斷變化的大小。 在現代處理器和操作系統中,它的確切工作方式無論如何都是非常抽象的,因此您通常不必擔心它的內在工作方式,除非(在允許它的語言中)您不得使用會您尚未分配或已釋放的內存。

  • 是什麼使速度更快?

    堆棧速度更快,因為所有可用內存始終是連續的。 無需維護所有可用內存段的列表,只需一個指向堆棧當前頂部的指針即可。 為此,編譯器通常將此指針存儲在特殊的快速寄存器中。 更重要的是,堆棧上的後續操作通常集中在內存的非常近的區域,這在非常低的級別上有利於通過處理器片上緩存進行優化。


86
投票

簡單來說,堆棧是在其中創建局部變量的地方。 同樣,每次調用子例程時,程序計數器(指向下一條機器指令的指針)和任何重要的寄存器,有時參數都被壓入堆棧。 然後,子例程中的所有局部變量都被壓入堆棧(並從那裡使用)。 子例程完成後,所有東西將從堆棧中彈出。 PC和註冊數據將被獲取並放回彈出位置,因此程序可以繼續進行。

堆是進行動態內存分配的內存區域(顯式的“ new”或“ allocate”調用)。 它是一種特殊的數據結構,可以跟踪大小不同的內存塊及其分配狀態。

在“經典”系統中,RAM的佈局使得堆棧指針從內存的底部開始,堆指針從內存的頂部開始,並且它們相互靠近。 如果它們重疊,則說明內存不足。 但是,這不適用於現代多線程操作系統。 每個線程都必須具有自己的堆棧,並且可以動態創建這些堆棧。


89
投票

您可以使用堆棧做一些有趣的事情。 例如,您具有類似alloca的功能(假設您可以繞過有關其使用的大量警告),這是一種malloc的形式,專門使用堆棧而不是堆來進行內存存儲。

也就是說,基於堆棧的內存錯誤是我遇到的最嚴重的錯誤。 如果使用堆內存,並且超出了分配的塊的邊界,則觸發分段錯誤的機會就很大。 (不是100%:您的塊可能偶然與您先前分配的另一個相鄰。)但是,由於在堆棧上創建的變量始終彼此相鄰,因此越界寫入可以更改另一個變量的值。 我了解到,每當我感到程序停止遵守邏輯規則時,它就有可能是緩衝區溢出。


80
投票

來自WikiAnwser。

當一個函數或方法調用另一個函數,然後又調用另一個函數等時,所有這些函數的執行將保持掛起狀態,直到最後一個函數返回其值為止。

掛起的函數調用鍊是堆棧,因為堆棧中的元素(函數調用)相互依賴。

在異常處理和線程執行中,必須考慮堆棧。

堆只是程序用於存儲變量的內存。 堆的元素(變量)彼此之間沒有依賴性,並且始終可以隨時隨地進行隨機訪問。


2270
投票

堆:

  • 就像堆一樣存儲在計算機RAM中。
  • 在堆棧上創建的變量將超出範圍並自動釋放。
  • 與堆中的變量相比,分配要快得多。
  • 用實際的堆棧數據結構實現。
  • 存儲用於參數傳遞的本地數據,返回地址。
  • 當使用過多的堆棧時(可能來自無限或太深的遞歸,非常大的分配),可能會導致堆棧溢出。
  • 可以在沒有指針的情況下使用在堆棧上創建的數據。
  • 如果您確切知道在編譯之前需要分配多少數據並且它不會太大,則可以使用堆棧。
  • 通常,程序啟動時已經確定了最大大小。

堆:

  • 像堆棧一樣存儲在計算機RAM中。
  • 在C ++中,必須手動銷毀堆上的變量,並且切勿超出範圍。 使用deletedelete[]free釋放數據。
  • 與堆棧上的變量相比,分配速度較慢。
  • 按需使用以分配數據塊以供程序使用。
  • 當有很多分配和釋放時,可能會產生碎片。
  • 在C ++或C中,在堆上創建的數據將由指針指向,並分別使用newmalloc分配。
  • 如果請求分配過多的緩衝區,可能會導致分配失敗。
  • 如果您不確切知道運行時需要多少數據或需要分配大量數據,則可以使用堆。
  • 負責內存洩漏。

例:

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;

201
投票

堆棧調用函數時,該函數的參數以及其他一些開銷會放在堆棧上。 一些信息(例如返回目的地)也存儲在此處。 當您在函數內聲明變量時,該變量也分配在堆棧上。

取消分配堆棧非常簡單,因為您總是以相反的順序進行分配。 輸入函數時會添加堆棧內容,退出它們時會刪除相應的數據。 這意味著除非您調用許多調用許多其他函數的函數(或創建遞歸解決方案),否則您往往會停留在堆棧的較小區域內。

該堆堆是你把你快速創建數據,其中的通用名稱。 如果您不知道程序將要創建多少個太空飛船,則很可能使用新的(或malloc或等效的)運算符來創建每個太空飛船。 這種分配將持續一段時間,因此很可能我們將以與創建它們不同的順序釋放事物。

因此,堆要復雜得多,因為最終會出現一些未使用的內存區域與塊相互交錯的情況-內存碎片化了。 尋找所需大小的可用內存是一個難題。 這就是為什麼應該避免使用堆的原因(儘管仍然經常使用堆)。

實現堆棧和堆的實現通常取決於運行時/ OS。 通常,性能至關重要的遊戲和其他應用程序會創建自己的內存解決方案,該解決方案從堆中獲取大量內存,然後在內部進行分配,以避免依賴OS來獲取內存。

僅當您的內存使用量與常規使用情況大不相同時(例如,對於您在一個大型操作中加載一個關卡並可以在另一個大型操作中將所有內容丟掉的遊戲),這才是實用的。

內存中的物理位置這與您想像的無關緊要,因為一項稱為“ 虛擬內存 ”的技術使您的程序認為您可以訪問物理數據在其他地方(甚至在硬盤上!)的某個地址。 隨著調用樹的不斷深入,您為堆棧獲取的地址按順序遞增。 堆的地址是不可預測的(即特定於隱含的),並且坦率地說並不重要。


112
投票

堆棧是內存的一部分,可以通過一些關鍵的彙編語言指令進行操作,例如“ pop”(從堆棧中刪除並返回值)和“ push”(將值推入堆棧),但也可以調用(調用一個子例程-這將地址壓入返回堆棧)並返回(從子例程返回-這會將地址從堆棧中彈出並跳轉到該地址)。 它是堆棧指針寄存器下方的內存區域,可以根據需要進行設置。 堆棧還用於將參數傳遞給子例程,還用於在調用子例程之前將值保留在寄存器中。

堆是操作系統通常通過諸如malloc之類的syscall分配給應用程序的一部分內存。 在現代OS上,此內存是僅呼叫進程有權訪問的一組頁面。

堆棧的大小是在運行時確定的,通常在程序啟動後不會增長。 在C程序中,堆棧必須足夠大以容納每個函數中聲明的每個變量。 堆將根據需要動態增長,但是操作系統最終會進行調用(它通常會使堆增長超過malloc請求的值,因此至少將來的某些malloc不需要返回內核來完成)。獲得更多的內存。這種行為通常可以自定義)

因為在啟動程序之前已經分配了堆棧,所以您不需要使用malloc就可以使用堆棧,因此那是一個小優點。 在實踐中,很難預測在具有虛擬內存子系統的現代操作系統中哪些將是快的,哪些將是慢的,因為頁面的實現方式和存儲位置是實現細節。


164
投票

其他人已經很好地回答了大筆劃,因此我將介紹一些細節。

  1. 堆棧和堆不必是單數。 擁有多個堆棧的一種常見情況是,如果一個進程中有多個線程。 在這種情況下,每個線程都有自己的堆棧。 您也可以有多個堆,例如某些DLL配置可能導致從不同堆分配不同的DLL,這就是為什麼釋放由不同庫分配的內存通常不是一個好主意的原因。

  2. 在C語言中,您可以通過使用alloca來獲得可變長度分配的好處, alloca可以在堆棧上進行分配,而alloc可以在堆上進行分配。 此內存將無法在return語句中保留下來,但是對於暫存緩衝區很有用。

  3. 在Windows上使用不多的巨大臨時緩衝區並不是免費的。 這是因為編譯器將生成一個堆棧探測循環,每次進入您的函數時都會調用該循環,以確保堆棧存在(因為Windows在堆棧末尾使用單個保護頁來檢測何時需要增大堆棧。如果您訪問的內存超出了堆棧末尾的一頁,則會崩潰。 例:

void myfunction()
{
   char big[10000000];
   // Do something that only uses for first 1K of big 99% of the time.
}

5775
投票

堆棧是為執行線程預留的暫存空間。 調用函數時,將在堆棧頂部保留一個塊,用於存放局部變量和一些簿記數據。 當該函數返回時,該塊將變為未使用狀態,並且可以在下次調用該函數時使用。 堆棧始終按LIFO(後進先出)順序保留; 最近保留的塊始終是下一個要釋放的塊。 這使得跟踪堆棧非常簡單。 從堆棧中釋放一個塊無非就是調整一個指針。

堆是為動態分配預留的內存。 與堆棧不同,堆中的塊分配和釋放沒有強制的模式。 您可以隨時分配一個塊,並隨時釋放它。 這使得跟踪在任何給定時間分配或釋放堆的哪些部分變得更加複雜。 有許多自定義堆分配器可用於調整不同使用模式的堆性能。

每個線程都有一個堆棧,而應用程序通常只有一個堆(儘管對於不同類型的分配有多個堆並不少見)。

要直接回答您的問題:

它們在多大程度上受操作系統或語言運行時的控制?

創建線程時,操作系統會為每個系統級線程分配堆棧。 通常,語言運行庫會調用OS來為應用程序分配堆。

他們的範圍是什麼?

堆棧連接到線程,因此當線程退出時,堆棧將被回收。 通常,堆是在運行時在應用程序啟動時分配的,並在應用程序(技術上已退出)退出時被回收。

什麼決定了它們的大小?

創建線程時設置堆棧的大小。 堆的大小是在應用程序啟動時設置的,但是可以隨需要的空間而增長(分配器從操作系統請求更多的內存)。

是什麼使速度更快?

堆棧速度更快,因為訪問模式使分配和釋放內存變得很簡單(指針/整數只是增加或減少),而堆的分配或釋放則要復雜得多。 同樣,堆棧中的每個字節都傾向於被非常頻繁地重用,這意味著它傾向於被映射到處理器的高速緩存中,從而使其非常快。 堆的另一個性能損失是,堆(通常是全局資源)通常必須是多線程安全的,即,每個分配和釋放都必須(通常)與程序中的“所有”其他堆訪問同步。

清晰的演示:
圖片來源: vikashazrati.wordpress.com


111
投票

我認為許多其他人在這個問題上給了您大多數正確的答案。

但是,遺漏的一個細節是“堆”實際上應該稱為“免費存儲”。 區別的原因是原始的免費存儲區是通過稱為“二項式堆”的數據結構實現的。 因此,從早期實現的malloc()/ free()進行分配是從堆進行分配。 但是,在當今的今天,大多數免費存儲都是用非常複雜的數據結構實現的,這些數據結構不是二項式堆。


131
投票

其他人直接回答了您的問題,但是當試圖了解堆棧和堆時,我認為考慮傳統UNIX進程(沒有線程和基於mmap()的分配器)的內存佈局會有所幫助。 內存管理詞彙表網頁上有此內存佈局的圖表。

傳統上,堆棧和堆位於進程的虛擬地址空間的相對兩端。 堆棧在訪問時自動增長,直至達到內核設置的大小(可以使用setrlimit(RLIMIT_STACK, ...)進行調整)。 當內存分配器調用brk()sbrk()系統調用,將更多的物理內存頁面映射到進程的虛擬地址空間時,堆就會增長。

在沒有虛擬內存的系統(例如某些嵌入式系統)中,通常使用相同的基本佈局,只是堆棧和堆的大小固定不變。 但是,在其他嵌入式系統(例如基於Microchip PIC微控制器的嵌入式系統)中,程序堆棧是單獨的存儲器塊,數據移動指令無法對其進行尋址,並且只能通過程序流指令(調用,返回等)。 其他體系結構(例如Intel Itanium處理器)具有多個堆棧 從這個意義上講,堆棧是CPU體系結構的元素。


©2020 sofbug.com - All rights reserved.