1. 程式人生 > >程式的記憶體分配之堆和棧的區別

程式的記憶體分配之堆和棧的區別

堆疊概述

  在計算機領域,堆疊是一個不容忽視的概念,堆疊是兩種資料結構。堆疊都是一種資料項按序排列的資料結構,只能在一端(稱為棧頂(top))對資料項進行插入和刪除。在微控制器應用中,堆疊是個特殊的儲存區,主要功能是暫時存放資料和地址,通常用來保護斷點和現場。

要點:
,優先佇列(priority queue);普通的佇列是一種先進先出的資料結構(FIFO—First-In/First-Out),元素在佇列尾追加,而從佇列頭刪除,(例如:乘車排隊,先來的排在前面先上車,後來的就要排的後面後上車; 哎,哎,你怎麼插隊呢,學沒學過佇列);在優先佇列中,元素被賦予優先順序。當訪問元素時,具有最高優先順序的元素最先取出。優先佇列具有最高階先出 (largest-in,first-out)的行為特徵。

,先進後出(FILO—First-In/Last-Out)(例如:超市排隊結賬,大一點的超市收銀臺都是一段狹長的過道,本來下一個是你了,突然這個收銀臺說不結了,OK,棧形成了,排在前面的要後出去了)。

一、程式的記憶體分配

1、一個由C/C++編譯的程式佔用的記憶體分為以下幾個部分

1)、棧區(stack)

由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其
操作方式類似於資料結構中的棧。

2)、堆區(heap)

一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回
收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。

3)、全域性區(靜態區)(static)

全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。 程式結束後由系統釋放。

4)、文字常量區

常量字串就是放在這裡的,程式結束後由系統釋放 。

5)、程式程式碼區

存放函式體的二進位制程式碼。

2、變數的儲存方式

這裡寫圖片描述
  首先,定義靜態變數時如果沒有初始化編譯器會自動初始化為0.。接下來,如果是使用常量表達式初始化了變數,則編譯器僅根據檔案內容(包括被包含的標頭檔案)就可以計算表示式,編譯器將執行常量表達式初始化。必要時,編譯器將執行簡單計算。如果沒有足夠的資訊,變數將被動態初始化。請看一下程式碼:

int global_1=1000;//靜態變數外部連結性常量表達式初始化
int global_2;//靜態變數外部連結性零初始化
static int one_file_1=1000;//靜態變數內部連結性常量表達式初始化
static int one_file_2;//靜態變數內部連結性零初始化
int main()
{
static int count_1=1000;//靜態變數無連結性常量表達式初始化
static int count_2;//靜態變數無連結性零初始化
return 0;
}

  所有的靜態持續變數都有下述初始化特徵:未被初始化的靜態變數的所有位都被設為0。這種變數被稱為零初始化。以上程式碼說明關鍵字static的兩種用法,但含義有些不同:用於區域性宣告,以指出變數是無連結性的靜態變數時,static表示的是儲存持續性;而用於程式碼塊外宣告時,static表示內部連結性,而變數已經是靜態持續性了。有人稱之為關鍵字過載,即關鍵字的含義取決於上下文。

二、C/C++堆和棧的區別

1.管理方式不同

棧,由編譯器自動管理,無需程式設計師手工控制;堆:產生和釋放由程式設計師控制。

2. 空間大小不同

棧的空間有限;堆記憶體可以達到4G,。

3. 能否產生碎片不同

棧不會產生碎片,因為棧是種先進後出的佇列。堆則容易產生碎片,多次的new/delete
會造成記憶體的不連續,從而造成大量的碎片。

4. 生長方向不同

堆的生長方式是向上的,棧是向下的。

5. 分配方式不同

堆是動態分配的。棧可以是靜態分配和動態分配兩種,但是棧的動態分配由編譯器釋放。

6. 快取級別不同:

  1)、棧使用的是一級快取, 他們通常都是被呼叫時處於儲存空間中,呼叫完畢立即釋放;
  2)、堆是存放在二級快取中,生命週期由虛擬機器的垃圾回收演算法來決定(並不是一旦成為孤兒物件就能被回收)。所以呼叫這些物件的速度要相對來得低一些。

7. 分配效率不同

  棧是機器系統提供的資料結構,計算機底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令。堆則是由C/C++函式庫提供,庫函式會按照一定的演算法在堆記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然後進行返回。顯然,堆的效率比棧要低得多。
  堆和棧相比,由於大量new/delete的使用,容易造成大量的記憶體碎片;由於沒有專門的系統支援,效率很低;由於可能引發使用者態和核心態的切換,記憶體的申請,代價變得更加昂貴。所以棧在程式中是應用最廣泛的,就算是函式的呼叫也利用棧去完成,函式呼叫過程中的引數,返回地址,EBP和區域性變數都採用棧的方式存放。所以,我們推薦大家 儘量用棧,而不是用堆。
  棧和堆相比不是那麼靈活,有時候分配大量的記憶體空間,還是用堆好一些。
  無論是堆還是棧,都要防止越界現象的發生。

例子程式

//main.cpp    
  int   a   =   0;   全域性初始化區    
  char   *p1;   全域性未初始化區    
  main()    
  {    
  int   b;   棧    
  char   s[]   =   "abc";   棧    
  char   *p2;   棧    
  char   *p3   =   "123456";   123456/0在常量區,p3在棧上。    
  static   int   c   =0;   全域性(靜態)初始化區    
  p1   =   (char   *)malloc(10);    
  p2   =   (char   *)malloc(20);    
  分配得來得1020位元組的區域就在堆區。    
  strcpy(p1,   "123456");   123456/0放在常量區,編譯器可能會將它與p3所指向的"123456"  
  優化成一個地方。    
  }    

三、java堆和棧的區別

1. 棧(stack)與堆(heap)都是Java用來在Ram中存放資料的地方。

與C++不同,Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。

2. 棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的暫存器。

但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。另外,棧資料在多個執行緒或者多個棧之間是不可以共享的,但是在棧內部多個值相等的變數是可以指向一個地址的,詳見第3點。堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。

3.Java中的資料型別有兩種。

一種是基本型別(primitivetypes), 共有8種,即int,short, long, byte, float, double, boolean, char(注意,並沒有string的基本型別)。這種型別的定義是通過諸如int a= 3; long b = 255L;的形式來定義的,稱為自動變數。值得注意的是,自動變數存的是字面值,不是類的例項,即不是類的引用,這裡並沒有類的存在。如int a= 3; 這裡的a是一個指向int型別的引用,指向3這個字面值。這些字面值的資料,由於大小可知,生存期可知(這些字面值固定定義在某個程式塊裡面,程式塊退出後,欄位值就消失了),出於追求速度的原因,就存在於棧中。
另外,棧有一個很重要的特殊性,就是存在棧中的資料可以共享。假設我們同時定義:

int a=3;
int b=3;

編譯器先處理int a= 3;首先它會在棧中建立一個變數為a的記憶體空間,然後查詢有沒有字面值為3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接著處理int b= 3;在建立完b的引用變數後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
特別注意的是,這種字面值的引用與類物件的引用不同。假定兩個類物件的引用同時指向一個物件,如果一個物件引用變數修改了這個物件的內部狀態,那麼另一個物件引用變數也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜尋棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
另一種是包裝類資料,【如Integer,String, Double等將相應的基本資料型別包裝起來的類。這些類資料全部存在於【堆】中】,Java用new()語句來顯示地告訴編譯器,在執行時才根據需要動態建立,因此比較靈活,但缺點是要佔用更多的時間。 4.String是一個特殊的包裝類資料。即可以用String str = new String(“abc”);的形式來建立,也可以用Stringstr = “abc”;的形式來建立(作為對比,在JDK 5.0之前,你從未見過Integer i = 3;的表示式,因為類與字面值是不能通用的,除了String。而在JDK5.0中,這種表示式是可以的!因為編譯器在後臺進行Integer i = new Integer(3)的轉換)。前者是規範的類的建立過程,即在Java中,一切都是物件,而物件是類的例項,全部通過new()的形式來建立。Java中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新建立的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的例項,只不過這個例項是在該類內部通過new()來建立的,而getInstance()向外部隱藏了此細節。那為什麼在String str = “abc”;中,並沒有通過new()來建立例項,是不是違反了上述原則?其實沒有。

4. 關於String str = “abc”的內部工作。

Java內部將此語句轉化為以下幾個步驟:【String str = “abc”,String str不要連著】
(1)先定義一個名為str的對String類的物件引用變數:String str;
(2)【在【棧】中查詢有沒有存放值為”abc”的地址,如果沒有,則開闢一個存放字面值為”abc”的地址,接著建立一個新的String類的物件o,並將o的字串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的物件o。如果已經有了值為”abc”的地址,則查詢物件o,並返回o的地址。】【上文說資料時存放在堆中,此文說資料存放在棧中】[因為此處不是通過new()建立的啊]
(3)將str指向物件o的地址。
值得注意的是,一般String類中字串值都是直接存值的。但像String str = “abc”;這種場合下,其字串值卻是儲存了一個指向存在棧中資料的引用!
為了更好地說明這個問題,我們可以通過以下的幾個程式碼進行驗證。

String str1="abc";
String str2="abc";
System.out.println(str1==str2);//true

注意,我們這裡並不用str1.equals(str2);的方式,因為這將比較兩個字串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個物件時才返回真值。而我們在這裡要看的是,str1與str2是否都指向了同一個物件。
  結果說明,JVM建立了兩個引用str1和str2,但只建立了一個物件,而且兩個引用都指向了這個物件。
我們再來更進一步,將以上程式碼改成:

String str1="abc";
String str2="abc";
str1="bcd";
System.out.println(str1+","+str2);//bcd,abc
System.out.println(str1==str2);//false

這就是說,賦值的變化導致了類物件引用的變化,str1指向了另外一個新物件!而str2仍舊指向原來的物件。上例中,當我們將str1的值改為”bcd”時,JVM發現在棧中沒有存放該值的地址,便開闢了這個地址,並建立了一個新的物件,其字串的值指向這個地址。
事實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在執行時根據新值悄悄建立了一個新物件,然後將這個物件的地址返回給原來類的引用。這個建立過程雖說是完全自動進行的,但它畢竟佔用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
再修改原來程式碼:

String str1="abc";
String str2="abc";
str1="bcd";
String str3=str1;
System.out.println(str3);//bcd
String str4="bcd";
System.out.println(str1==str4);//true

我們再接著看以下的程式碼。

String str1 = new String("abc"); 
  String str2 = "abc"; 
  System.out.println(str1==str2); //false
String str1 = "abc"; 
  String str2 = new String("abc"); 
  System.out.println(str1==str2); //false 

建立了兩個引用。建立了兩個物件。兩個引用分別指向不同的兩個物件。
以上兩段程式碼說明,只要是用new()來新建物件的,都會在堆中建立,而且其字串是單獨存值的,即使與棧中的資料相同,也不會與棧中的資料共享。

5. 資料型別包裝類的值不可修改。

不僅僅是String類的值不可修改,所有的資料型別包裝類都不能更改其內部的值。

6. 結論與建議:

(1)我們在使用諸如String str = “abc”;的格式定義類時,總是想當然地認為,我們建立了String類的物件str。擔心陷阱!物件可能並沒有被建立!唯一可以肯定的是,指向String類的引用被建立了。至於這個引用到底是否指向了一個新的物件,必須根據上下文來考慮,除非你通過new()方法來顯要地建立一個新的物件。因此,更為準確的說法是,我們建立了一個指向String類的物件的引用變數str,這個物件引用變數指向了某個值為”abc”的String類。清醒地認識到這一點對排除程式中難以發現的bug是很有幫助的。
(2)使用String str = “abc”;的方式,可以在一定程度上提高程式的執行速度,因為JVM會自動根據棧中資料的實際情況來決定是否有必要建立新物件。而對於Stringstr = new String(“abc”);的程式碼,則一概在堆中建立新物件,而不管其字串值是否相等,是否有必要建立新物件,從而加重了程式的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裡實現是否應用了這個模式,不得而知。
(3)當比較包裝類裡面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個物件時,用==。
(4)由於String類的immutable性質,當String變數需要經常變換其值時,應該考慮使用StringBuffer類,以提高程式效率

四、堆和棧的理論知識

1、申請方式

stack:
由系統自動分配。 例如,宣告在函式中一個區域性變數 int b; 系統自動在棧中為b開闢空間
heap:
需要程式設計師自己申請,並指明大小,在c中malloc函式
如p1 = (char *)malloc(10);
在C++中用new運算子
如p2 = new char[10];
但是注意p1、p2本身是在棧中的。

2、申請後系統的響應

棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。
堆:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,程式碼中的delete語句才能正確的釋放本記憶體空間。
另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中。

3、申請大小的限制

棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

4、申請效率的比較:

棧由系統自動分配,速度較快。但程式設計師是無法控制的。
堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,他不是在堆,也不是在棧是
直接在程序的地址空間中保留一塊記憶體,雖然用起來最不方便。但是速度快,也最靈活。

5、堆和棧中的儲存內容

棧: 在函式呼叫時,首先進棧的是函式的各個引數,然後是主函式中後的下一條指令(函式呼叫語句的下一條可執行語句)的地址;在大多數的C編譯器中,引數是由右往左入棧的(為什麼是由右往左入棧的?),然後是函式中的區域性變數。注意靜態變數是不入棧的(存放在靜態區)
當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續執行。
堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容由程式設計師安排。

6、存取效率的比較

char s1[] = “aaaaaaaaaaaaaaa”;
char *s2 = “bbbbbbbbbbbbbbbbb”;
aaaaaaaaaaa是在執行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = “1234567890”;
char *p =”1234567890”;
a = c[1];
a = p[1];
return;
}
對應的彙編程式碼
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,再根據edx讀取字元,顯然慢了。

7、小結:

堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。