memory-management stack不同 - 堆棧和堆的內容和位置是什麼?




heap array (21)

編程語言書籍解釋了在堆棧上創建了值類型,並且在堆上創建了引用類型,而沒有解釋這兩者是什麼。 我還沒有看到對此的明確解釋。 我理解堆棧是什麼。 但,

  • 它們在哪里和它們(物理上在真實計算機的記憶中)?
  • 它們在多大程度上受操作系統或語言運行時控制?
  • 它們的範圍是什麼?
  • 是什麼決定了它們的大小?
  • 是什麼讓一個更快?

Answers

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

在堆棧上保存返回地址並調用→push / ret→pop直接在硬件中管理。

您可以使用堆棧傳遞參數..即使它比使用寄存器慢(微處理器大師會說或者20世紀80年代的BIOS書籍......)

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

堆棧使用速度更快:

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

由於一些答案被挑剔,我將貢獻我的蟎蟲。

令人驚訝的是,沒有人提到多個(即與運行的OS級線程的數量無關)調用堆棧不僅可以在外來語言(PostScript)或平台(Intel Itanium)中找到,還可以在fibers綠色線程中找到coroutines一些實現。

纖維,綠線和協同程序在很多方面都很相似,這導致了很多混亂。光纖和綠色線程之間的區別在於前者使用協作式多任務處理,而後者可能具有協作式或搶占式(或甚至兩者)。有關纖維和協同程序之間的區別,請參見here

在任何情況下,光纖,綠色線程和協同程序的目的是同時執行多個函數,但不是並行執行(在這個OS級別的線程中參見此問題,以區分),從而相互傳遞控制權以有組織的方式。

使用光纖,綠色線程或協程時,每個函數通常都有一個單獨的堆棧。 (從技術上講,不僅僅是一個堆棧,而是整個執行上下文是每個函數。最重要的是,CPU寄存器。)對於每個線程,存在與並發運行函數一樣多的堆棧,並且線程在執行每個函數之間切換根據你的程序的邏輯。當一個函數運行到它的結尾時,它的堆棧就會被銷毀。因此,堆棧的數量和生命週期是動態的,並不是由OS級線程的數量決定的!

請注意,我說“ 每個函數通常有一個單獨的堆棧”。有倆都stackful無堆疊 couroutines的實現。最值得注意的stackful C ++實現是Boost.Coroutine微軟PPLasync/await。(然而,C ++的可恢復函數(又名“ asyncawait”),它們被提議用於C ++ 17,可能會使用無堆棧協程。)

纖維對C ++標準庫的提議即將發布。還有一些第三方libraries。綠色線程在Python和Ruby等語言中非常流行。


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

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

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

直接回答您的問題:

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

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

它們的範圍是什麼?

堆棧附加到一個線程,因此當線程退出堆棧時將被回收。 堆通常在應用程序啟動時由運行時分配,並在應用程序(技術過程)退出時回收。

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

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

是什麼讓一個更快?

堆棧更快,因為訪問模式使得從中分配和釋放內存變得微不足道(指針/整數簡單地遞增或遞減),而堆在分配或釋放中涉及更複雜的簿記。 此外,堆棧中的每個字節都經常被頻繁地重用,這意味著它往往被映射到處理器的緩存,使其非常快。 堆的另一個性能影響是堆(主要是全局資源)通常必須是多線程安全的,即每個分配和釋放需要 - 通常 - 與程序中的“所有”其他堆訪問同步。

明確的示範:
圖片來源: vikashazrati.wordpress.com


簡單地說,堆棧是創建局部變量的地方。此外,每次調用子程序時,程序計數器(指向下一個機器指令的指針)和任何重要的寄存器,有時參數都會被壓入堆棧。然後子程序中的任何局部變量都被壓入堆棧(並從那裡使用)。當子程序結束時,所有東西都會從堆棧中彈出。PC和寄存器數據會隨著彈出而被放回原位,因此您的程序可以順利進行。

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

在“經典”系統中,RAM被佈置成使得堆棧指針從內存的底部開始,堆指針從頂部開始,並且它們朝向彼此增長。如果它們重疊,則表示RAM不足。但這不適用於現代多線程操作系統。每個線程都必須有自己的堆棧,這些堆棧可以動態創建。


在20世紀80年代,UNIX像兔子一樣傳播,大公司自己推銷。埃克森只有一個,歷史上丟失了數十個品牌。如何記憶是由許多實施者自行決定的。

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

單個STACK通常是HEAP下面的一個區域,它是一個內存區域,直到下一個固定內存塊的頂部才包含任何有價值的內容。下一個塊通常是CODE,它可以在其時代的一個著名黑客中被堆棧數據覆蓋。

一個典型的存儲器塊是BSS(零值塊),在一個製造商的產品中意外沒有歸零。另一個是包含初始化值的DATA,包括字符串和數字。第三個是包含CRT(C運行時),main,函數和庫的CODE。

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


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


在以下C#代碼中

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

以下是內存的管理方式

只要函數調用進入堆棧,只需要持續的Local Variables 。 堆用於變量,我們事先並不知道它們的生命週期,但我們希望它們可以持續一段時間。 在大多數語言中,如果我們想要將它存儲在堆棧中,那麼在編譯時我們知道變量的大小是至關重要的。

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

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

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

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

和這裡:

在堆棧和堆上創建對象

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

但請注意,它可能包含一些不准確之處。


堆棧當你調用一個函數時,該函數的參數加上一些其他開銷被放在堆棧上。 一些信息(例如返回的地方)也存儲在那裡。 在函數內部聲明變量時,該變量也會在堆棧中分配。

取消分配堆棧非常簡單,因為您總是按照分配的相反順序解除分配。 輸入函數時會添加堆棧內容,退出時會刪除相應的數據。 這意味著您傾向於保持在堆棧的一個小區域內,除非您調用許多調用許多其他函數的函數(或創建遞歸解決方案)。

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

因此,堆要復雜得多,因為最終存在未使用的內存區域與內存被分段的塊交織。 找到所需大小的空閒內存是一個難題。 這就是應該避免堆的原因(雖然它仍然經常使用)。

實現堆棧和堆的實現通常都是運行時/操作系統。 通常,性能至關重要的遊戲和其他應用程序會創建自己的內存解決方案,從堆中獲取大量內存,然後在內部將其清除,以避免依賴操作系統獲取內存。

這只有在你的內存使用量與標準大不相同的情況下才有用 - 例如,對於你在一個巨大的操作中加載一個級別並且可以在另一個巨大的操作中丟掉所有內存的遊戲。

內存中的物理位置由於虛擬內存技術使您的程序認為您可以訪問物理數據位於其他位置的某個地址(甚至在硬盤上!),因此與您的想法相關性較低。 隨著調用樹的深入,您獲得的堆棧地址會逐漸增加。 堆的地址是不可預測的(即特定於implimentation),坦率地說並不重要。


在排序

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

詳細地

堆棧

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

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

更多可以在here找到。

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

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

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

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

更多可以在here找到。

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

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

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

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

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

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

甚至,herehere給出here更多細節。

現在來看你的問題的答案

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

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

更多可以在here找到。

它們的範圍是什麼?

已經在頂部給出。

“如果你確切地知道在編譯之前需要分配多少數據,你就可以使用堆棧。它不是太大。如果你不確切知道在運行時需要多少數據,你可以使用堆。你需要分配大量的數據。“

更多信息可以在here找到。

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

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

是什麼讓一個更快?

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

此外,堆棧與堆不僅是性能考慮因素; 它還告訴你很多關於對象的預期壽命。

細節可以在here找到。


其他人直接回答了你的問題,但在嘗試理解堆棧和堆時,我認為考慮傳統UNIX進程的內存佈局(沒有線程和mmap()基於分配器)是有幫助的。“ 內存管理術語表”網頁提供了此內存佈局的圖表。

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

在沒有虛擬內存的系統中,例如某些嵌入式系統,通常會應用相同的基本佈局,但堆棧和堆的大小是固定的。但是,在其他嵌入式系統(例如基於Microchip PIC單片機的系統)中,程序堆棧是一個單獨的內存塊,無法通過數據移動指令尋址,只能通過程序流指令間接修改或讀取(調用,返回等)。其他架構(如Intel Itanium處理器)具有多個堆棧。從這個意義上說,堆棧是CPU架構的一個元素。


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

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

但是,通常最好考慮“ 範圍 ”和“ 生命週期 ”而不是“堆棧”和“堆積”。

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

生命週期是指在程序執行期間分配和取消分配變量的時間。通常我們會想到靜態分配(變量將持續整個程序的持續時間,使其對於在多個函數調用中存儲相同的信息很有用)與自動分配(變量僅在單個函數調用期間持續存在,使其對於存儲僅在函數期間使用的信息,並且一旦完成就可以丟棄)與動態分配(變量的持續時間在運行時定義,而不是像靜態或自動一樣的編譯時)。

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

我將提供一些簡單的帶註釋的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

(我已經從另一個或多或少是這個問題的問題中提出了這個答案。)

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

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

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

堆棧

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

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

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

如何管理堆實際上取決於運行時環境。 C使用malloc而C ++使用new ,但許多其他語言都有垃圾收集。

但是,堆棧是與處理器架構緊密相關的更低級別的功能。 當沒有足夠的空間時增加堆不是太難,因為它可以在處理堆的庫調用中實現。 但是,堆棧的增長通常是不可能的,因為只有在為時已晚時才發現堆棧溢出; 並且關閉執行線程是唯一可行的選擇。


好吧,簡單而言之,它們意味著有序不是有序 ......!

堆棧:在堆棧項目中,事物處於彼此的頂部,意味著要更快,更有效地處理!...

所以總有一個索引指向特定項目,處理也會更快,項目之間也有關係!...

:沒有訂單,處理速度會慢,價值混亂,沒有特定的訂單或索引......有隨機的,它們之間沒有關係...所以執行和使用時間可能會有所不同......

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


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

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

其他人已經很好地回答了廣泛的筆觸,所以我會提出一些細節。

  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.
}

堆:

  • 像堆一樣存儲在計算機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;

  • 介紹

物理存儲器是存儲器單元的物理地址的範圍,其中應用程序或系統在執行期間存儲其數據,代碼等。內存管理表示通過將數據從物理內存交換到存儲設備然後在需要時返回物理內存來管理這些物理地址。 OS使用虛擬內存實現內存管理服務。作為C#應用程序開發人員,您無需編寫任何內存管理服務。 CLR使用底層操作系統內存管理服務為C#或任何其他針對CLR的高級語言提供內存模型。

圖4-1顯示了使用虛擬內存概念由OS抽象和管理的物理內存。虛擬內存是由OS管理的物理內存的抽象視圖。虛擬內存只是一系列虛擬地址,這些虛擬地址在需要時由CPU轉換為物理地址。

圖4-1。 CLR內存抽象

CLR使用操作內存服務為虛擬執行環境提供內存管理抽象層。CLR使用的抽象概念是AppDomain,線程,堆棧,堆存儲映射文件等。應用程序域(AppDomain)的概念為您的應用程序提供了一個獨立的執行環境。

  • CLR和OS之間的內存交互

通過在調試以下C#應用程序時查看堆棧跟踪,使用WinDbg,您將看到CLR如何使用底層操作系統內存管理服務(例如,來自KERNEL32.dll的HeapFree方法,來自ntdll.dll的RtlpFreeHeap方法)來實現自己的記憶模型:

using System;
namespace CH_04
{
    class Program
    {
        static void Main(string[] args)
        {
            Book book = new Book();
            Console.ReadLine();
        }
    }

    public class Book
    {
        public void Print() { Console.WriteLine(ToString()); }
    }
}

程序的編譯程序集被加載到WinDbg中以開始調試。您可以使用以下命令初始化調試會話:

0:000> sxe ld clrjit

0:000> g

0:000> .loadby sos clr

0:000> .load C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ sos.dll

然後,使用!bpmd命令在Program類的Main方法中設置斷點:

0:000>!bpmd CH_04.exe CH_04.Program.Main

要繼續執行並在斷點處中斷,請執行g命令:

0:000> g

當執行在斷點處中斷時,使用!eestack命令查看為當前進程運行的所有線程的堆棧跟踪詳細信息。以下輸出顯示了為應用程序CH_04.exe運行的所有線程的堆棧跟踪:

0:000>!eestack

線程0

當前幀:(MethodDesc 00233800 +0 CH_04.Program.Main(System.String []))

ChildEBP RetAddr Caller,Callee

0022ed24 5faf21db clr!CallDescrWorker + 0x33

/ trace刪除 /

0022f218 77712d68 ntdll!RtlFreeHeap + 0x142,調用ntdll!RtlpFreeHeap

0022f238 771df1ac KERNEL32!HeapFree + 0x14,調用ntdll!RtlFreeHeap

0022f24c 5fb4c036 clr!EEHeapFree + 0x36,調用KERNEL32!HeapFree

0022f260 5fb4c09d clr!EEHeapFreeInProcessHeap + 0x24,調用clr!EEHeapFree

0022f274 5fb4c06d clr!operator delete [] + 0x30,調用clr!EEHeapFreeInProcessHeap / trace刪除 /

0022f4d0 7771316f ntdll!RtlpFreeHeap + 0xb7a,調用ntdll!_SEH_epilog4

0022f4d4 77712d68 ntdll!RtlFreeHeap + 0x142,調用ntdll!RtlpFreeHeap

0022f4f4 771df1ac KERNEL32!HeapFree + 0x14,調用ntdll!RtlFreeHeap

/ trace刪除 /

此堆棧跟踪指示CLR使用OS內存管理服務來實現其自己的內存模型。.NET中的任何內存操作都通過CLR內存層傳輸到OS內存管理層。

圖4-2說明了CLR在運行時使用的典型C#應用程序內存模型。

圖4-2。典型的C#應用程序內存模型

CLR內存模型與OS內存管理服務緊密耦合。要了解CLR內存模型,了解底層操作系統內存模型非常重要。了解物理內存地址空間如何被抽像到虛擬內存地址空間,用戶應用程序和系統應用程序使用虛擬地址空間的方式,虛擬到物理地址映射如何工作,內存如何也是至關重要的。映射文件有效,等等。這些背景知識將提高您對CLR內存模型概念的掌握,包括AppDomain,堆棧和堆。

有關更多信息,請參閱本書:

C#解構:了解C#如何在.NET Framework上運行

本書+ ClrViaC#+ Windows Internals是已知的.net框架的深入資源和與OS的關係的優秀資源。


我有一些東西要與你分享,雖然已經寫了重點。

  • 非常快速的訪問。
  • 存儲在RAM中。
  • 這裡加載函數調用以及傳遞的局部變量和函數參數。
  • 當程序超出範圍時,會自動釋放空間。
  • 存儲在順序存儲器中。

  • 相對於Stack而言訪問速度較慢。
  • 存儲在RAM中。
  • 動態創建的變量存儲在此處,以後需要在使用後釋放分配的內存。
  • 存儲在內存分配的任何位置,始終由指針訪問。

有趣的說明:

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

反饋很好。


我想很多其他人在這件事上給了你大部分正確答案。

然而,遺漏的一個細節是“堆”實際上可能被稱為“免費商店”。這種區別的原因是原始的免費存儲是使用稱為“二項式堆”的數據結構實現的。因此,從malloc()/ free()的早期實現中分配是從堆中分配的。然而,在這個現代,大多數免費商店都使用非二維堆的非常精細的數據結構來實現。


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

堆是操作系統給應用程序的內存的一部分,通常通過類似malloc的系統調用。在現代操作系統上,此內存是一組只有調用進程才能訪問的頁面。

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

因為你在啟動程序之前已經分配了堆棧,所以在使用堆棧之前你永遠不需要malloc,所以這是一個小優勢。在實踐中,很難預測具有虛擬內存子系統的現代操作系統的速度和速度會有多快,因為頁面的實現方式和存儲位置是實現細節。


垃圾收集很昂貴。 您只希望它盡可能少地運行。 理想情況下從不。 因此,系統將盡可能延遲垃圾收集,基本上直到你的內存不足為止。

分配內存很昂貴。 一旦運行時分配了一些內存,它通常不會再次釋放它,即使它當前不需要它,因為如果它在程序運行時間的一個時間內需要那麼多內存,則可能需要它在將來的某個時間,類似的內存量,並希望避免再次分配內存。

因此,即使在測試期間發生了垃圾收集,您也不會在任務管理器或進程資源管理器中看到它,因為無論如何CLR都不會釋放它。

您所描述的內容稱為引用計數垃圾收集器 。 但是,CLI VES的所有當前現有實現都使用跟踪GC 。 跟踪GC不計算參考; 他們追踪它們, 只有當它們在運行時 。 跟踪GC在實際跟踪對像圖之前不會注意到對像是否仍然可訪問,並且只有在需要運行集合時才會跟踪對像圖,即當內存不足時。





memory-management language-agnostic stack heap