8522
1770
編程語言書籍解釋了值類型是在堆棧上創建的,而引用類型是在堆上創建的,而沒有說明這兩個是什麼。我還沒有閱讀清楚的解釋。我了解堆棧是什麼。但,
它們在哪里和什麼位置(物理上在真實計算機的內存中)?
它們在多大程度上受操作系統或語言運行時的控制?
他們的範圍是什麼?
什麼決定了它們的大小?
是什麼使速度更快? 
堆棧是為執行線程預留的暫存空間。調用函數時,將在堆棧頂部保留一個塊,用於存放局部變量和一些簿記數據。當該函數返回時,該塊將變為未使用狀態,並且可以在下次調用該函數時使用。堆棧始終按LIFO(先進先出)順序保留;最近保留的塊始終是要釋放的下一個塊。這使得跟踪堆棧非常簡單。從堆棧中釋放一個塊無非就是調整一個指針。
堆是為動態分配預留的內存。與堆棧不同,堆中的塊分配和釋放沒有強制的模式。您可以隨時分配一個塊,並隨時釋放它。這使跟踪在任何給定時間分配或釋放堆的哪些部分變得更加複雜。有許多自定義堆分配器可用於針對不同的使用模式調整堆性能。
每個線程都有一個堆棧,而應用程序通常只有一個堆(儘管對於不同類型的分配有多個堆並不少見)。
要直接回答您的問題:
它們在多大程度上受操作系統或語言運行時的控制?
創建線程時,操作系統會為每個系統級線程分配堆棧。通常,語言運行庫會調用OS來為應用程序分配堆。
他們的範圍是什麼?
堆棧連接到線程,因此當線程退出時,堆棧將被回收。通常,堆是在運行時在應用程序啟動時分配的,並在應用程序(技術上已退出)退出時被回收。
什麼決定了它們的大小?
創建線程時設置堆棧的大小。堆的大小是在應用程序啟動時設置的,但是可以隨需要的空間而增加(分配器從操作系統請求更多的內存)。
是什麼使速度更快?
堆棧速度更快,因為訪問模式使從中分配內存和取消分配內存變得微不足道(指針/整數只是遞增或遞減),而堆的分配或釋放則涉及到更為複雜的簿記。另外,堆棧中的每個字節都傾向於非常頻繁地重用,這意味著它傾向於被映射到處理器的高速緩存中,從而使其非常快。堆的另一個性能問題是,通常是全局資源的堆通常必須是多線程安全的,即,每個分配和釋放都必須(通常)與程序中“所有”其他堆訪問同步。
清晰的演示:
圖片來源:vikashazrati.wordpress.com
|
堆棧:
就像堆一樣存儲在計算機RAM中。
在堆棧上創建的變量將超出範圍並自動釋放。
與堆中的變量相比,分配要快得多。
用實際的堆棧數據結構實現。
存儲用於參數傳遞的本地數據,返回地址。
當使用過多的堆棧時(可能來自無限或太深的遞歸,非常大的分配),可能會導致堆棧溢出。
可以在沒有指針的情況下使用在堆棧上創建的數據。
如果您確切地知道在編譯之前需要分配多少數據並且它不會太大,則可以使用堆棧。
通常,程序啟動時已經確定了最大大小。
堆:
像堆棧一樣存儲在計算機RAM中。
在C ++中,必須手動銷毀堆上的變量,並且切勿超出範圍。使用delete,delete []或free釋放數據。
與堆棧上的變量相比,分配速度較慢。
按需使用以分配數據塊以供程序使用。
當有很多分配和釋放時,可能會產生碎片。
在C ++或C中,堆上創建的數據將由指針指向,並分別使用new或malloc進行分配。
如果請求分配過多的緩衝區,可能會導致分配失敗。
如果您不確切知道運行時需要多少數據或需要分配大量數據,則可以使用堆。
負責內存洩漏。
例:
int foo()
{
char * pBuffer; // <-尚未分配任何東西(不包括指針本身,後者在堆棧中分配)。
bool b = true; //分配在堆棧上。
如果(b)
{
//在堆棧上創建500個字節
字符緩衝區[500];
//在堆上創建500個字節
pBuffer =新字符[500];
} // <-緩衝區在這裡釋放,pBuffer不在
} // <---哎呀,這是內存洩漏,我應該叫delete [] pBuffer;
|
最重要的一點是,堆和堆棧是內存分配方式的通用術語。它們可以以許多不同的方式實現,並且這些術語適用於基本概念。
在一堆物品中,物品按照放置在另一個物品上的順序排列在另一個物品的頂部,並且您只能刪除頂部的物品(不會把整個事情推翻)。
堆棧的簡單性在於您不需要維護一個表,該表包含分配的內存的每個部分的記錄。您唯一需要的狀態信息是指向堆棧末尾的單個指針。要分配和取消分配,您只需遞增和遞減該單個指針。注意:有時可以將堆棧實現為從內存部分的頂部開始,然後向下擴展而不是向上擴展。
在堆中,項的放置方式沒有特定的順序。因為沒有清晰的“頂部”項目,所以您可以按任意順序進入和移除項目。
堆分配需要完整記錄分配的內存和未分配的內存,並進行一些開銷維護以減少碎片,找到足夠大以適合請求的大小的連續內存段,依此類推。可以隨時釋放內存以留下可用空間。有時,內存分配器將執行維護任務,例如通過移動分配的內存來對內存進行碎片整理或垃圾回收-在運行時識別內存不再在作用域中並對其進行分配。
這些映像應該在描述堆棧和堆中分配和釋放內存的兩種方式方面做得相當好。好吃
它們在多大程度上受操作系統或語言運行時的控制?
如前所述,堆和堆棧是通用術語,可以通過多種方式實現。計算機程序通常具有稱為調用堆棧的堆棧,該堆棧存儲與當前功能有關的信息,例如指向從哪個函數調用的指針以及任何局部變量。由於函數先調用其他函數然後返回,所以堆棧會不斷擴大和縮小,以保存來自函數的信息,這些信息將在調用堆棧的更下方。一個程序實際上並沒有對它的運行時控制。它由編程語言,操作系統甚至系統架構決定。
堆是用於動態且隨機分配的任何內存的通用術語。即出現故障。內存通常由OS分配,應用程序調用API函數進行此分配。管理動態分配的內存需要相當大的開銷,通常由所使用的編程語言或環境的運行時代碼來處理。
他們的範圍是什麼?
調用棧是一個低級的概念,從編程的意義上講它與“作用域”無關。如果您分解一些代碼,則會看到指向堆棧部分的相對指針樣式引用,但是就高級語言而言,該語言強加了自己的作用域規則。但是,堆棧的一個重要方面是,一旦函數返回,該函數本地的所有內容都會立即從堆棧中釋放出來。鑑於您的編程語言是如何工作的,因此可以按照您期望的方式工作。在堆中,也很難定義。範圍是操作系統公開的任何內容,但是您的編程語言可能會添加有關其在應用程序中的“作用域”的規則。處理器體系結構和操作系統使用虛擬尋址,虛擬尋址將處理器轉換為物理地址,並且存在頁面錯誤等。它們跟踪哪些頁面屬於哪些應用程序。但是,您根本不需要擔心這一點,因為您只需使用編程語言用來分配和釋放內存的任何方法,並檢查錯誤(如果分配/釋放由於任何原因而失敗)。
什麼決定了它們的大小?
同樣,它取決於語言,編譯器,操作系統和體系結構。堆棧通常是預先分配的,因為根據定義,它必須是連續的內存。語言編譯器或操作系統確定其大小。您不會在堆棧上存儲大量數據,因此它將足夠大,以至於永遠不要完全使用它,除非發生不必要的無窮遞歸(因此,“堆棧溢出”)或其他異常編程決策。
堆是可以動態分配的任何事物的通用術語。根據您看待它的方式,它會不斷變化的大小。在現代處理器和操作系統中,它的確切工作方式無論如何都是非常抽象的,因此您通常不必擔心它的內在工作方式,除非(在允許它的語言中)您不得使用會您尚未分配或已釋放的內存。
是什麼使速度更快?
堆棧速度更快,因為所有可用內存始終是連續的。無需維護所有可用內存段的列表,只需一個指向堆棧當前頂部的指針即可。為此,編譯器通常將此指針存儲在特殊的快速寄存器中。更重要的是,堆棧上的後續操作通常集中在內存的非常近的區域中,這在非常低的水平上有利於通過處理器上的處理器進行優化緩存。
|
(我將此答案從另一個或多或少是這個問題的重複的問題上移開了。)
您問題的答案是特定於實現的,並且可能因編譯器和處理器體系結構而異。但是,這是一個簡化的說明。
堆棧和堆都是從底層操作系統分配的內存區域(通常是虛擬內存,按需映射到物理內存)。
在多線程環境中,每個線程將具有其自己的完全獨立的堆棧,但它們將共享堆。並發訪問必須在堆上進行控制,而在堆棧上則不可能。
堆
堆包含已用和可用塊的鏈接列表。堆上的新分配(通過new或malloc)是通過從一個空閒塊中創建一個合適的塊來滿足的。這需要更新堆上的塊列表。有關堆上塊的元信息通常也存儲在堆上每個塊前面的小區域中。
隨著堆的增長,通常將新塊從低地址分配到高地址。因此,您可以將堆視為存儲塊堆,隨著分配的內存,存儲塊的大小會增加。如果堆對於分配而言太小,則通常可以通過從底層操​​作系統獲取更多內存來增加大小。
分配和釋放許多小塊可能會使堆處於這樣一種狀態,即在已使用的塊之間散佈著許多小空閒塊。分配一個大塊的請求可能會失敗,因為即使這些空閒塊的組合大小可能足夠大,也沒有一個空閒塊足以滿足分配請求。這稱為堆碎片。
當與空閒塊相鄰的已用塊被釋放時,新的空閒塊可以與相鄰的空閒塊合併以創建更大的空閒塊,從而有效地減少了堆的碎片。
堆棧
堆棧通常與CPU上一個名為堆棧指針的特殊寄存器緊密配合使用。最初,堆棧指針指向堆棧的頂部(堆棧中的最高地址)。
CPU具有用於將值壓入堆棧並將其從堆棧彈出的特殊指令。每次推送都會將值存儲在堆棧指針的當前位置,並減小堆棧指針。 pop檢索堆棧指針所指向的值,然後增加堆棧指針(不要因向堆棧中添加值會減少堆棧指針,而刪除值會增加堆棧指針這一事實而感到困惑。請記住,堆棧會增長為底端)。存儲和檢索的值是CPU寄存器的值。
調用函數時,CPU使用特殊指令來推送當前指令指針,即在堆棧上執行的代碼的地址。然後,通過設置CPU跳轉到該功能。
指令指針指向所調用函數的地址。稍後,當函數返回時,舊的指令指針會從堆棧中彈出,並在調用函數後立即在代碼處恢復執行。
輸入函數後,將減少堆棧指針,以在堆棧上為本地(自動)變量分配更多空間。如果函數具有一個局部32位變量,則在堆棧上預留​​4個字節。當函數返回時,將堆棧指針移回以釋放分配的區域。
如果函數具有參數,則在調用函數之前將它們壓入堆棧。函數中的代碼然後可以從當前堆棧指針向上瀏覽堆棧以找到這些值。
嵌套函數調用的工作方式就像一種魅力。每個新的調用將分配函數參數,局部變量的返回地址和空間,並且這些激活記錄可以堆疊用於嵌套調用,並在函數返回時以正確的方式展開。
由於堆棧是有限的內存塊,因此您可能會通過調用過多的嵌套函數和/或為局部變量分配過多的空間而導致堆棧溢出。通常,用於堆棧的存儲區的設置方式是,在堆棧底部(最低地址)以下進行寫入將觸發CPU陷阱或異常。然後,運行時可以捕獲這種異常情況,並將其轉換為某種堆棧溢出異常。
可以在堆而不是堆棧上分配函數嗎?
否,函數(即本地或自動變量)的激活記錄分配在堆棧上,該記錄不僅用於存儲這些變量,還用於跟踪嵌套的函數調用。
堆的管理方式實際上取決於運行時環境。 C使用malloc,C ++使用new,但是許多其他語言具有垃圾回收。
但是,堆棧是與處理器體系結構緊密相關的更底層的功能。在沒有足夠空間的情況下增長堆並不難,因為它可以在處理堆的庫調用中實現。但是,堆棧的增長通常是不可能的,因為只有在為時已晚時才發現堆棧溢出。並關閉執行線程是唯一可行的選擇。
|
在下面的C#代碼中
公共無效Method1()
{
int i = 4;
整數y = 2;
class1 cls1 =新的class1();
}
這是管理內存的方式
僅需要持續函數調用的局部變量就進入堆棧。堆用於變量,這些變量的壽命我們並不是很早就知道,但是我們希望它們能持續一段時間。在大多數語言中,至關重要的是,我們要在編譯時知道要將變量存儲在堆棧中的大小。
對象(隨著我們更新它們的大小而變化)會進入堆,因為我們在創建時不知道它們將持續多長時間。在許多語言中,堆都是通過垃圾回收來查找不再具有任何引用的對象(例如cls1對象)。
在Java中,大多數對象直接進入堆。在諸如C / C ++之類的語言中,當您不處理指針時,結構和類通常可以保留在堆棧中。
更多信息可以在這裡找到:
堆棧和堆內存分配之間的區別«timmurphy.org
和這裡:
在堆棧和堆上創建對象
本文是上述圖片的來源:六個重要的.NET概念:堆棧,堆,值類型,引用類型,裝箱和拆箱-CodeProject
但請注意,其中可能包含一些錯誤。
|
堆棧
當您調用一個函數時,該函數的參數以及其他一些開銷會放在堆棧上。一些信息(例如返回目的地)也存儲在此處。
當您在函數內聲明變量時,該變量也分配在堆棧上。
取消分配棧非常簡單,因為您總是以相反的順序進行分配。輸入函數時會添加堆棧內容,退出函數時會刪除相應的數據。這意味著除非您調用許多調用許多其他函數的函數(或創建遞歸解決方案),否則您往往會停留在堆棧的較小區域內。
堆
堆是用於將創建的數據動態放置的位置的通用名稱。如果您不知道程序將要創建多少個太空飛船,則很可能使用新的(或malloc或等效的)運算符來創建每個太空飛船。這種分配將持續一段時間,因此很可能我們將以與創建它們不同的順序釋放事物。
因此,堆要復雜得多,因為最終會出現一些未使用的內存區域與塊相互交錯的情況-內存碎片化了。尋找所需大小的可用內存是一個難題。這就是為什麼應該避免使用堆的原因(儘管仍然經常使用堆)。
實作
堆棧和堆的實現通常取決於運行時/ OS。通常,性能至關重要的遊戲和其他應用程序會創建自己的內存解決方案,該解決方案從堆中獲取大量內存,然後在內部進行分配,以避免依賴OS來獲取內存。
僅當您的內存使用量與常規使用情況大不相同時(例如,對於您在一次大型操作中加載一個關卡並在另一次大型操作中可以佔用全部內存的遊戲),這才是實用的。
內存中的物理位置
這與您想像的不太相關,因為有一種稱為虛擬內存的技術,該技術使您的程序認為您可以訪問物理數據在其他地方(甚至在硬盤上!)的某個地址。隨著調用樹的深入,您為堆棧獲取的地址將按升序排列。堆的地址是不可預測的(即特定於隱含的),坦率地說並不重要。
|
為了澄清,此答案的信息不正確(托馬斯在評論後將其答案固定,很酷:))。其他答案只是避免解釋靜態分配的含義。因此,我將在下面解釋三種主要的分配形式以及它們通常與堆,堆棧和數據段的關係。我還將在C / C ++和Python中顯示一些示例,以幫助人們理解。
未在堆棧上分配“靜態”(又稱靜態分配)變量。不要這麼假設-許多人這樣做只是因為“靜態”聽起來很像“堆棧”。它們實際上既不存在於堆棧中,也不存在於堆中。是所謂的數據段的一部分。
但是,通常最好考慮“範圍”和“生存期”,而不是“堆棧”和“堆”。
範圍是指代碼的哪些部分可以訪問變量。通常,我們認為本地範圍(只能由當前函數訪問)與全局範圍(可以在任何地方訪問)相比,儘管範圍會變得更加複雜。
生存期是指在程序執行期間何時分配和釋放變量。通常我們認為靜態分配(可變會在程序的整個過程中持續存在,這對於在多個函數調用中存儲相同信息很有用)與自動分配(變量僅在單次調用函數期間保持不變,使其對存儲僅在您的過程中使用的信息有用)功能,可以在完成後就丟棄)與動態分配(變量的持續時間在運行時定義,而不是像靜態或自動那樣在編譯時定義)。
儘管大多數編譯器和解釋器在使用堆棧,堆等方面都類似地實現了此行為,但是只要行為正確,編譯器有時可能會破壞這些約定。例如,由於優化,即使大多數局部變量存在於堆棧中,局部變量也可能僅存在於寄存器中或被完全刪除。正如一些評論中指出的那樣,您可以自由地實現一個甚至不使用堆棧或堆,而是使用其他一些存儲機制的編譯器(很少用,因為堆棧和堆非常有用)。
我將提供一些簡單的帶註釋的C代碼來說明所有這些。最好的學習方法是在調試器下運行程序並觀察行為。如果您喜歡閱讀python,請跳到答案的結尾:)
//首次加載程序/ DLL時在數據段中靜態分配
//當程序/ DLL退出時釋放
//範圍-可以從代碼中的任何位置訪問
int someGlobalVariable;
//首次加載程序時在數據段中靜態分配
//當程序/ DLL退出時釋放
//範圍-可以從此特定代碼文件中的任何位置訪問
static int someStaticVariable;
//每次調用MyFunction時,都會在堆棧上分配“ someArgument”
//當MyFunction返回時,“ someArgument”被釋放
//範圍-只能在MyFunction()中訪問
void MyFunction(int someArgument){
//首次加載程序時在數據段中靜態分配
//當程序/ DLL退出時釋放
//範圍-只能在MyFunction()中訪問
static int someLocalStaticVariable;
//每次調用MyFunction時都分配在堆棧上
//當MyFunction返回時釋放
//範圍-只能在MyFunction()中訪問
int someLocalVariable;
//每次調用MyFunction時,在堆棧上分配一個* pointer *
//當MyFunction返回時,該指針被釋放
//作用域-只能在MyFunction()中訪問指針
int * someDynamicVariable;
//此行導致在堆中分配整數的空間
//執行此行時。請注意,這不是開始
//調用MyFunction(),就像自動變量一樣
//範圍-只有MyFunction()中的代碼才能訪問該空間
// *通過此特定變量*。
//但是,如果您將地址傳遞到其他地方,則該代碼
//也可以訪問
someDynamicVariable = new int;
//此行為堆中的整數釋放空間。
//如果我們不寫它,內存將被“洩漏”。
//注意堆棧和堆之間的根本區別
//必須對堆進行管理。堆棧是為我們管理的。
刪除someDynamicVariable;
//在其他情況下,您無需釋放此堆空間,
//可以將地址存儲在更永久的位置,以便以後使用。
//有些語言甚至為您處理了釋放問題..​​.但是
//總是需要在某種機制下在運行時進行維護。
//當函數返回時,someArgument,someLocalVariable
//和指針someDynamicVariable被釋放。
// someDynamicVariable指向的空間已經
//在返回之前釋放。
返回;
}
//請注意someGlobalVariable,someStaticVariable和
// someLocalStaticVariable繼續存在,但不存在
//釋放,直到程序退出。
為什麼區分生命週期和作用域很重要的一個特別令人毛骨悚然的示例是變量可以具有局部作用域,但具有靜態生命週期-例如,上面的代碼示例中的“ someLocalStaticVariable”。這樣的變量會使我們常見但非正式的命名習慣非常混亂。例如,當我們說“本地”時,通常是指“本地範圍的自動分配的變量”,而當我們說“全局”時,通常是指“全局範圍的靜態分配的變量”。不幸的是,當涉及到“文件範圍內的靜態分配的變量”之類的事情時,許多人只是說...“呵呵?
C / C ++中的某些語法選擇會加劇此問題-例如,由於以下所示的語法,許多人認為全局變量不是“靜態”的。
int var1; //具有全局作用域和靜態分配
static int var2; //具有文件範圍和靜態分配
int main(){返回0;}
請注意,在上面的聲明中添加關鍵字“ static”可防止var2具有全局作用域。但是,全局var1具有靜態分配。這不是直觀!因此,在描述範圍時,我盡量不要使用“靜態”一詞,而應說“文件”或“文件受限”範圍之類的東西。但是,許多人使用短語“靜態”或“靜態作用域”來描述只能從一個代碼文件訪問的變量。就生存期而言,“靜態”始終表示在程序啟動時分配變量,並在程序退出時釋放變量。
有些人認為這些概念是C / C ++特有的。他們不是。例如,下面的Python示例說明了所有三種分配類型(在解釋性語言中可能存在一些細微的差異,我將不在這裡介紹)。
從datetime導入datetime
動物類:
_FavoriteFood ='未定義'#_FavoriteFood是靜態分配的
def PetAnimal(自己):
curTime = datetime.time(datetime.now())#curTime自動分配
print(“謝謝你撫摸我。但是它是” + str(curTime)+“,你應該餵我。我最喜歡的食物是” + self._FavoriteFood)
貓(動物)類:
_FavoriteFood ='tuna'#注意,由於我們進行了覆蓋,因此Cat類具有自己的靜態分配的_FavoriteFood變量,與Animal的變量不同
狗類(動物):
_FavoriteFood ='steak'#同樣,Dog類獲得其自己的靜態變量。重要說明-這個靜態變量在Dog的所有實例之間共享,因此它不是動態的!
如果__name__ ==“ __main__”:
晶須= Cat()#動態分配
fido = Dog()#動態分配
rinTinTin = Dog()#動態分配
whickers.PetAnimal()
fido.PetAnimal()
rinTinTin.PetAnimal()
Dog._FavoriteFood ='milkbones'
whickers.PetAnimal()
fido.PetAnimal()
rinTinTin.PetAnimal()
#輸出為:
#謝謝你撫摸我。但這是13:05:02.255000,您應該餵我。我最喜歡的食物是金槍魚
#謝謝你撫摸我。但這是13:05:02.255000,您應該餵我。我最喜歡的食物是牛排
#謝謝你撫摸我。但這是13:05:02.255000,您應該餵我。我最喜歡的食物是牛排
#謝謝你撫摸我。但這是13:05:02.255000,您應該餵我。我最喜歡的食物是金槍魚
#謝謝你撫摸我。但這是13:05:02.255000,您應該餵我。我最喜歡的食物是牛奶
#謝謝你撫摸我。但這是13:05:02.256000,您應該餵我。我最喜歡的食物是牛奶
|
其他人已經很好地回答了大招,因此我將介紹一些細節。
堆棧和堆不必是單個的。您擁有多個堆棧的一種常見情況是,如果一個進程中有多個線程。在這種情況下,每個線程都有自己的堆棧。您也可以有多個堆,例如某些DLL配置可能導致從不同堆分配不同的DLL,這就是為什麼釋放不同庫分配的內存通常不是一個好主意的原因。
在C語言中,您可以通過使用alloca來獲得可變長度分配的好處,該分配在堆棧上進行分配,而不是在堆上進行分配。此內存無法在您的return語句中保留下來,但對於暫存緩衝區很有用。
在Windows上使用不多的巨大臨時緩衝區並不是免費的。這是因為編譯器將生成一個堆棧探測循環,每次進入您的函數以確保堆棧存在時都會調用該循環(因為Windows在堆棧末尾使用單個保護頁來檢測何時需要增大堆棧。如果您訪問的內存距離堆棧末尾多了一頁,則會崩潰。例:
無效myfunction()
{
char big [10000000];
//做一些僅在前99%的時間中用於前1K的操作。
}
|
其他人直接回答了您的問題,但是當試圖了解堆棧和堆時,我認為考慮傳統UNIX進程(沒有線程和基於mmap()的分配器)的內存佈局會有所幫助。內存管理詞彙表網頁上有此內存佈局的圖表。
傳統上,堆棧和堆位於進程的虛擬地址空間的相對兩端。堆棧在訪問時會自動增長,直至達到內核設置的大小(可以使用setrlimit(RLIMIT_STACK,...)進行調整)。當內存分配器調用brk()或sbrk()系統調用,將更多的物理內存頁面映射到進程的虛擬地址空間時,堆就會增長。
在沒有虛擬內存的系統(例如某些嵌入式系統)中,通常使用相同的基本佈局,只是堆棧和堆的大小固定不變。但是,在其他嵌入式系統(例如基於Microchip PIC微控制器的嵌入式系統)中,程序堆棧是單獨的內存塊,數據移動指令無法對其進行尋址,並且只能通過程序流指令(調用,返回等)。其他體系結構(例如Intel Itanium處理器)具有多個堆棧。從這個意義上講,堆棧是CPU體系結構的元素。
|
堆棧是一部分可以通過幾種關鍵的彙編語言指令來操作的內存,例如“ pop”(從堆棧中刪除並返回值)和“ push”(將值推入堆棧),但也可以調用(調用子例程-壓入地址以返回到堆棧)並返回(從子例程返回-這將地址從堆棧中彈出並跳轉到該地址)。它是堆棧指針寄存器下方的內存區域,可以根據需要進行設置。堆棧還用於將參數傳遞給子例程,還用於在調用子例程之前將值保留在寄存器中。
堆是操作系統通常通過諸如malloc之類的syscall分配給應用程序的一部分內存。在現代OS上,此內存是只有調用進程才能訪問的一組頁面。
堆棧的大小是在運行時確定的,通常在程序啟動後不會增長。在C程序中,堆棧必須足夠大以容納每個函數中聲明的每個變量。堆將根據需要動態增長,但是操作系統最終會進行調用(它通常會使堆增長超過malloc請求的值,因此至少將來的某些malloc不需要返回內核來完成)獲得更多的內存。這種行為通常可以自定義)
因為在啟動程序之前已經分配了堆棧,所以您不需要使用malloc就可以使用堆棧,因此那是一個小優點。在實踐中,很難預測在具有虛擬內存子系統的現代操作系統中哪些將是快的,哪些將是慢的,因為頁面的實現方式和存儲位置是實現細節。
|
什麼是堆棧?
堆棧是一堆物體,通常是整齊排列的物體。
計算體系結構中的堆棧是存儲器的區域,在其中以後進先出的方式添加或刪除數據。
在多線程應用程序中,每個線程將具有自己的堆棧。
什麼是堆?
堆是隨意堆放的東西的不整潔集合。
在計算體系結構中,堆是由操作系統或內存管理器庫自動管理的動態分配內存區域。
堆內存在程序執行期間定期分配,釋放和調整大小,這可能會導致稱為碎片的問題。
當在內存對象之間分配的很小的空間太小而無法容納其他內存對象時,就會發生碎片。
最終結果是無法用於進一步的內存分配的堆空間的百分比。
兩者一起
在多線程應用程序中,每個線程將具有自己的堆棧。但是,所有不同的線程將共享堆。
由於不同的線程在多線程應用程序中共享堆,因此這也意味著線程之間必須進行某種協調,以使它們不會嘗試訪問和操作堆中相同的內存。同一時間。
哪個更快-堆棧還是堆?又為什麼呢
堆棧比堆快得多。
這是因為在堆棧上分配內存的方式。
在堆棧上分配內存就像向上移動堆棧指針一樣簡單。
對於剛接觸編程的人來說,使用堆棧可能是個好主意,因為它比較容易。
因為堆棧很小,所以當您確切知道數據將需要多少內存時,或者如果您知道數據的大小很小時,您可能希望使用它。
當您知道您的數據將需要大量內存,或者您不確定需要多少內存(例如動態數組)時,最好使用堆。
Java內存模型
堆棧是存儲局部變量(包括方法參數)的內存區域。當涉及對像變量時,這些僅僅是對堆上實際對象的引用(指針)。
每次實例化一個對象時,都會將一大堆堆內存放在一邊以保存該對象的數據(狀態)。由於對象可以包含其他對象,因此某些數據實際上可以保存對這些嵌套對象的引用。
|
我認為許多其他人在這個問題上給了您大多數正確的答案。
但是,遺漏的一個細節是“堆”實際上應該稱為“免費存儲”。區別的原因是原始的免費存儲區是通過稱為“二項式堆”的數據結構實現的。因此,從早期實現的malloc()/ free()進行分配是從堆進行分配。但是,在當今的今天,大多數免費存儲都是使用非常複雜的數據結構實現的,這些數據結構不是二項式堆。
|
您可以使用堆棧做一些有趣的事情。例如,您具有諸如alloca之類的功能(假設您可以繞過有關其使用的大量警告),這是一種malloc形式,專門使用堆棧而不是堆來存儲。
也就是說,基於堆棧的內存錯誤是我經歷過的最嚴重的錯誤。如果使用堆內存,並且超出了分配的塊的邊界,則觸發分段錯誤的機會就很大。 (不是100%:您的塊可能偶然與您先前分配的另一個相鄰。)但是,由於在堆棧上創建的變量始終彼此相鄰,因此越界寫入可以更改另一個變量的值。我了解到,每當我感到程序停止遵守邏輯規則時,它就有可能是緩衝區溢出。
|
簡單來說,堆棧是在其中創建局部變量的地方。同樣,每次調用子例程時,程序計數器(指向下一條機器指令的指針)和任何重要的寄存器,有時參數都被壓入堆棧。然後,子例程中的所有局部變量都被壓入堆棧(並從那裡使用)。子例程結束後,所有東西將從堆棧中彈出。 PC和註冊數據將被獲取並放回彈出位置,因此程序可以繼續進行。
堆是進行動態內存分配的內存區域(顯式“新”或“分配”調用)。它是一種特殊的數據結構,可以跟踪大小不同的內存塊及其分配狀態。
在“經典”系統中,RAM的佈局使得堆棧指針從內存的底部開始,堆指針從內存的頂部開始,並且它們相互靠近。如果它們重疊,則說明內存不足。但是,這不適用於現代多線程操作系統。每個線程都必須具有自己的堆棧,並且可以動態創建這些堆棧。
|
來自WikiAnwser。
疊放
當一個函數或方法調用另一個函數,然後又調用另一個函數等時,所有這些函數的執行將保持掛起狀態,直到最後一個函數返回其值為止。
掛起的函數調用鍊是堆棧,因為堆棧中的元素(函數調用)相互依賴。
在異常處理和線程執行中,堆棧是重要的考慮因素。
堆
堆只是程序用於存儲變量的內存。
堆中的元素(變量)彼此之間沒有依賴性,並且始終可以隨時隨意訪問。
|
疊放
快速訪問
不必顯式取消分配變量
CPU有效管理空間,內存不會碎片化
僅局部變量
堆棧大小限制(取決於OS)
變量無法調整大小
堆
變量可以全局訪問
內存大小無限制
(相對)訪問速度較慢
無法保證有效利用空間,隨著時間的流逝,隨著分配內存塊,然後釋放內存,內存可能會碎片化
您必須管理內存(您負責分配和釋放變量)
可以使用realloc()調整變量的大小
|
簡而言之
堆棧用於存儲靜態內存,堆用於存儲動態內存,兩者均存儲在計算機的RAM中。
詳細
堆棧
堆棧是一個“ LIFO”(後進先出)數據結構,該結構由CPU非常緊密地管理和優化。每當函數聲明一個新變量時,它都會“推送”到堆棧中。然後,每次函數退出時,該函數壓入堆棧的所有變量都將被釋放(也就是說,它們將被刪除)。釋放堆棧變量後,該內存區域將可用於其他堆棧變量。
使用堆棧存儲變量的優點是可以為您管理內存。您不必手動分配內存,也可以在不再需要它時釋放它。此外,由於CPU如此高效地組織堆棧存儲器,因此讀取和寫入堆棧變量的速度非常快。
在這裡可以找到更多。
堆
堆是計算機內存中不會自動為您管理的區域,並且不受CPU嚴格管理。它是內存中更自由浮動的區域(並且更大)。要在堆上分配內存,必須使用內置的C函數malloc()或calloc()。在堆上分配內存後,一旦不再需要使用free()負責分配該內存。
如果不這樣做,程序將發生所謂的內存洩漏。也就是說,堆上的內存仍將被保留(並且其他進程將無法使用)。正如我們將在調試部分中看到的那樣,有一個名為Valgrind的工具可以幫助您檢測內存洩漏。
與堆棧不同,堆對可變大小沒有大小限制(除了計算機的明顯物理限制之外)。堆內存的讀取和寫入速度稍慢,因為必須使用指針來訪問堆上的內存。我們將在短期內討論指針。
不像堆疊堆中創建的變量可由程序中任何位置的任何函數訪問。堆變量本質上是全局範圍的。
在這裡可以找到更多。
在堆棧上分配的變量直接存儲到內存中,並且對該內存的訪問非常快,並且在編譯程序時會處理其分配。當一個函數或方法調用另一個函數,然後又調用另一個函數等時,所有這些函數的執行將保持掛起狀態,直到最後一個函數返回其值為止。堆棧始終按LIFO順序保留,最近保留的塊始終是要釋放的下一個塊。這使得跟踪堆棧真的非常簡單,從堆棧中釋放一個塊只不過是調整一個指針而已。
堆上分配的變量在運行時分配了內存,訪問該內存的速度稍慢,但是堆大小僅受虛擬內存大小的限制。堆中的元素彼此之間沒有依賴關係,並且始終可以隨時隨意訪問。您可以隨時分配一個塊,並隨時釋放它。這使得跟踪在任何給定時間分配或釋放堆的哪些部分變得更加複雜。
如果您確切知道在編譯之前需要分配多少數據,並且它不會太大,則可以使用堆棧。如果您不確切知道運行時將需要多少數據,或者是否需要分配大量數據,則可以使用堆。
在多線程情況下,每個線程將具有其自己的完全獨立的堆棧,但它們將共享堆。堆棧是特定於線程的,堆是特定於應用程序的。在異常處理和線程執行中,堆棧是重要的考慮因素。
每個線程都有一個堆棧,而應用程序通常只有一個堆(儘管對於不同類型的分配有多個堆並不少見)。
在運行時,如果應用程序需要更多的堆,則可以從可用內存中分配內存,如果堆棧需要內存,則可以從可用內存中為應用程序分配的內存中分配內存。
甚至,這里和這裡都給出了更多細節。
現在來回答您的問題。
它們在多大程度上受操作系統或語言運行時的控制?
創建線程時,操作系統會為每個系統級線程分配堆棧。通常,語言運行庫會調用OS來為應用程序分配堆。
在這裡可以找到更多。
他們的範圍是什麼?
已經在頂部。
“可以使用堆棧,前提是您知道在編譯之前需要分配多少數據,並且它不會太大。如果您不知道在運行時確切需要多少數據,或者您需要分配大量數據。”
在這裡可以找到更多。
什麼決定了它們的大小?
創建線程時,堆棧大小由OS設置。堆的大小是在應用程序啟動時設置的,但是它可以根據需要的空間而增長(分配器從操作系統請求更多的內存)。
是什麼使速度更快?
堆棧分配要快得多,因為它真正要做的只是移動堆棧指針。使用內存池,可以從堆分配中獲得可比的性能,但這會帶來一點點複雜性和麻煩。
另外,堆棧與堆不僅是性能方面的考慮,而且還需要考慮性能。它還告訴您很多有關對象的預期壽命的信息。
可以從這裡找到詳細信息。
|
好吧,簡單地說,它們的意思是有序而不是有序...!
堆放:在堆放物品中,東西相互疊放,意味著將更快,更高效地進行處理!...
因此總會有一個索引來指向特定的項目,而且處理也會更快,項目之間也有關係!...
堆:沒有順序,處理會變慢,並且值被混亂在一起,沒有特定的順序或索引...是隨機的,它們之間沒有關係...因此執行和使用時間可能會有所不同...
我還創建了下面的圖像,以顯示它們的外觀:
|
虛擬內存中每個進程的堆棧,堆和數據:
|
在1980年代,UNIX像小兔子一樣傳播,大公司也自己滾動。
埃克森美孚(Exxon)擁有一個,而幾十個失去歷史的品牌也是如此。
內存的佈局方式由許多實現者決定。
一個典型的C程序平放在內存中
通過更改brk()值來增加機會。
通常,HEAP剛好低於此brk值
brk的增加會增加可用堆的數量。
單個STACK通常是HEAP之下的一塊區域,這是一塊內存
直到下一個固定內存塊的頂部為止,它不包含任何值。
下一個塊通常是CODE,可能會被堆棧數據覆蓋
在那個時代著名的黑客之一中。
一個典型的存儲塊是BSS(零塊)值)
偶然地沒有在一個製造商的產品中歸零。
另一個是包含初始化值的DATA,包括字符串和數字。
第三個是包含CRT(C運行時),主要,函數和庫的CODE。
UNIX中虛擬內存的出現改變了許多限制。
沒有客觀原因可以說明這些塊必須是連續的,
或大小固定,或現在以特定方式訂購。
當然,在UNIX出現之前,Multics不受這些限制。
這是一個示意圖,顯示了那個時代的一種存儲器佈局。
|
幾分錢:我認為,以圖形方式繪製內存並且更簡單會很好:
箭頭-通常在線程創建API中通過參數顯示增長堆棧和堆的位置,進程堆棧大小的限制(在OS中定義),線程堆棧大小的限制。堆通常受進程最大虛擬內存大小的限制,例如32位2-4 GB。
如此簡單的方式:進程堆是進程及其內部所有線程的通用對象,通常在諸如malloc()之類的情況下用於內存分配。
堆棧是快速存儲器,用於在通常情況下存儲函數返回指針和變量,並作為函數調用中的參數(局部函數變量)進行處理。
|
由於有些答案很挑剔,所以我將貢獻自己的一份力量。
令人驚訝的是,沒有人提到,不僅在異類語言(PostScript)或平台(Intel Itanium)中,而且在光纖,綠色線程中都發現了多個(即與正在運行的OS級線程的數量無關)調用堆棧。以及協程的一些實現。
纖維,生絲和協程在許多方面都相似,這導致很多混亂。光纖和綠色線程之間的區別在於,前者使用協作式多任務處理,而後者則可以採用協作式或搶占式(或什至兩者)。有關纖維和協程之間的區別,請參見此處。
無論如何,光纖,綠色線程和協程的目的是在一個OS級線程中同時執行多個功能,但不能並行執行(有關區別,請參見此SO問題),從而相互之間來回傳遞控制以有組織的方式。
使用纖維,綠線或協程時,通常每個功能都有單獨的堆棧。 (從技術上講,每個函數不僅是堆棧,而且整個執行上下文是每個函數。最重要的是,CPU寄存器。)對於每個線程,堆棧與同時運行的函數一樣多,並且線程在執行每個函數之間切換根據您程序的邏輯。當函數運行到末尾時,其堆棧將被破壞。因此,堆棧的數量和生存期是動態的,而不是由OS級線程的數量決定的!
請注意,我說“通常每個函數有一個單獨的堆棧”。協程既有堆棧實現又有堆棧實現。最著名的堆棧式C ++實現是Boost.Coroutine和Microsoft PPL的async / await。 (但是,C ++ 17提出的C ++可恢復函數(又稱“異步並等待”)很可能使用無堆棧協程。)
即將向C ++標準庫提出光纖建議。另外,還有一些第三方庫。綠色線程在Python和Ruby等語言中非常流行。
|
儘管已經涵蓋了主要要點,但我有一些要分享的內容。
疊放
快速訪問。
存儲在RAM中。
函數調用與傳遞的局部變量和函數參數一起加載到此處。
當程序超出範圍時,將自動釋放空間。
存儲在順序存儲器中。
堆
相對較慢地訪問堆棧。
存儲在RAM中。
動態創建的變量存儲在此處,以後需要使用後釋放分配的內存。
存儲在完成內存分配的任何位置,始終由指針訪問。
有趣的注意事項:
如果將函數調用存儲在堆中,則會導致2個混亂點:
由於堆棧中的順序存儲,因此執行速度更快。堆中的存儲將導致大量時間消耗,從而使整個程序的執行速度變慢。
如果函數存儲在堆中(指針指向的混亂存儲),則將無法返回到調用者地址(由於內存中的順序存儲,所以堆棧可以返回)。
|
哇!答案如此之多,我認為其中沒有一個是正確的...
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
|
許多答案在概念上都是正確的,但我們必須注意,硬件(即微處理器)需要一個堆棧才能允許調用子例程(彙編語言中的CALL。)。 (OOP傢伙會稱之為方法)
在堆棧上,您可以保存返回地址並調用→推入/退出→彈出窗口直接在硬件中進行管理。
您可以使用堆棧來傳遞參數..即使它比使用寄存器要慢(微處理器專家或一本好的1980年代BIOS書籍都會這樣)。
沒有堆棧,微處理器將無法工作。 (我們無法想像沒有子例程/函數的程序,即使是彙編語言)
沒有堆就可以。 (彙編語言程序可以不使用而工作,因為堆是OS概念,而malloc是OS / Lib調用。
堆棧使用速度更快,因為:
是硬件,甚至push / pop都非常有效。
malloc需要進入內核模式,使用鎖/信號量(或其他同步原語)執行一些代碼,並管理一些跟踪分配所需的結構。
|
堆是由操作系統或內存管理器庫自動管理的動態分配內存區域。您可以隨時分配一個塊,並隨時釋放它。堆分配需要完整記錄分配的內存和未分配的內存,並進行一些開銷維護以減少碎片,找到足夠大以適合請求的大小的連續內存段,依此類推。可以隨時釋放內存以留下可用空間。隨著堆的增長,通常將新塊從低地址分配到高地址。因此,您可以將堆視為存儲塊堆,隨著分配的內存,存儲塊的大小會增加。如果堆對於分配而言太小,則通常可以通過從底層操​​作系統獲取更多內存來增加大小。從堆分配的內存將保持分配狀態,直到發生以下情況之一:
內存已釋放
程序終止
堆棧:
就像堆一樣存儲在計算機RAM中。
在堆棧上創建的變量將超出範圍並自動釋放。
與堆中的變量相比,分配要快得多。
存儲用於參數傳遞的本地數據,返回地址。
當使用過多堆棧時(大多數情況下可能會產生堆棧溢出)
從無限或太深的遞歸,非常大的分配)。
如果您確切知道需要多少數據,則可以使用堆棧
在編譯時間之前分配,並且不會太大。
通常在您的程序確定時具有最大大小
開始。
堆:
像堆棧一樣存儲在計算機RAM中。
在C ++中,必須手動銷毀堆上的變量,並且永遠不要
超出範圍。
使用delete,delete []或free釋放數據。
與堆棧上的變量相比,分配速度較慢。
按需使用以分配數據塊以供程序使用。
當分配很多時可能會產生碎片,並且
釋放。
在C ++或C中,在堆上創建的數據將由指針指向
並分別用new或malloc分配。
如果請求太大的緩衝區可能會導致分配失敗
被分配。
您如果您不知道要多少數據,將使用堆
將在運行時或需要分配大量數據時使用。
負責內存洩漏。
|
堆棧本質上是一個易於訪問的內存,僅管理其項目
作為一個很好的堆棧。只有事先知道尺寸的物品才能進入堆棧。數字,字符串和布爾值就是這種情況。
堆是您無法預先確定項目的內存
確切的尺寸和結構。由於對象和數組可以被突變和
在運行時更改時,它們必須進入堆。
資料來源:Academind
|
CPU堆棧和堆在物理上與CPU和寄存器與內存的工作方式,機器彙編語言的工作方式有關,而不與高級語言本身有關,即使這些語言可以決定什麼。
所有現代CPU都遵循“相同”微處理器理論:它們全部基於所謂的“寄存器”,而有些則用於“堆棧”以獲得性能。從一開始,所有CPU都有堆棧寄存器,據我所知,它們一直都在這裡。彙編語言從一開始就一樣,儘管有所不同...直到Microsoft及其中間語言(IL)才改變了範例,使其具有OO虛擬機彙編語言。因此,將來我們將能夠擁有一些CLI / CIL CPU(MS的一個項目)。
CPU具有堆棧寄存器以加快內存訪問速度,但與使用其他寄存器以獲得對進程的所有可用內存的完全訪問權限相比,它們受到限制。這就是為什麼我們談論堆棧和堆分配的原因。
總而言之,總的來說,堆是緩慢且緩慢的,並且用於“全局”實例和對象內容,因為堆棧又小又快,並且用於“局部”變量和引用(隱藏指針以忘記管理它們)。
因此,當我們在方法中使用new關鍵字時,將在堆棧中創建引用(一個int),但如果記得,則將在堆中創建對象及其所有內容(值類型和對象)。但是局部基本值類型和數組是在堆棧中創建的。
內存訪問的區別在於單元引用級別:尋址堆(進程的整體存儲器)在處理CPU寄存器方面需要更多的複雜性,而堆棧在尋址方面要“更多”,因為CPU堆棧在本地我記得,寄存器用作基址。
這就是為什麼當我們有很長或無限長的遞歸調用或循環時,我們很快就會發生堆棧溢出,而沒有凍結現代計算機上的系統的情況。
.NET中的C#堆與堆棧
堆棧與堆:了解差異
將靜態類內存分配存儲在C#中
什麼是堆棧和堆?
https://zh.wikipedia.org/wiki/內存管理
https://zh.wikipedia.org/wiki/Stack_register
彙編語言資源:
彙編編程教程
英特爾®64和IA-32架構軟件開發人員手冊
|
感謝您進行了很好的討論,但是作為一個真正的菜鳥,我想知道將指令保存在哪裡?在開始階段,科學家們在兩種架構之間做出決定(馮·紐曼(von NEUMANN)將所有內容都視為數據,而哈佛(HARVARD)則將內存區域用於指令,而另一區域則用於數據)。最終,我們採用了馮·諾依曼(von Neumann)的設計,現在一切都被認為是“相同的”。當我學習彙編時,這使我很難
https://www.cs.virginia.edu/~evans/cs216/guides/x86.html
因為他們談論寄存器和堆棧指針。
以上所有內容都談到了DATA。我的猜測是,由於指令是具有特定內存佔用空間的已定義事物,因此它將進入堆棧,因此彙編中討論的所有“那些”寄存器都位於堆棧中。當然,然後是面向對象的程序設計,其中指令和數據混合成一個動態的結構,因此指令現在也將保留在堆中嗎?
|
高度活躍的問題。贏得10個聲譽才能回答這個問題。信譽要求有助於保護該問題免受垃圾郵件和非答復活動的侵害。
不是您要找的答案?瀏覽其他標記為內存管理堆棧語言不可知的堆動態內存分配的問題,或者詢問您自己的問題。