程序的内存分配之堆和栈的区别
堆棧概述
??在計算機領域,堆棧是一個不容忽視的概念,堆棧是兩種數據結構。堆棧都是一種數據項按序排列的數據結構,只能在一端(稱為棧頂(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.。接下來,如果是使用常量表達式初始化了變量,則編譯器僅根據文件內容(包括被包含的頭文件)就可以計算表達式,編譯器將執行常量表達式初始化。必要時,編譯器將執行簡單計算。如果沒有足夠的信息,變量將被動態初始化。請看一下代碼:
??所有的靜態持續變量都有下述初始化特征:未被初始化的靜態變量的所有位都被設為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); 分配得來得10和20字節的區域就在堆區。 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;首先它會在棧中創建一個變量為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”;這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用!
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
注意,我們這里并不用str1.equals(str2);的方式,因為這將比較兩個字符串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這里要看的是,str1與str2是否都指向了同一個對象。
結果說明,JVM創建了兩個引用str1和str2,但只創建了一個對象,而且兩個引用都指向了這個對象。
我們再來更進一步,將以上代碼改成:
這就是說,賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們將str1的值改為”bcd”時,JVM發現在棧中沒有存放該值的地址,便開辟了這個地址,并創建了一個新的對象,其字符串的值指向這個地址。
事實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運行時根據新值悄悄創建了一個新對象,然后將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
再修改原來代碼:
我們再接著看以下的代碼。
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、小結:
堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
總結
以上是生活随笔為你收集整理的程序的内存分配之堆和栈的区别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: docker安装并运行ElasticSe
- 下一篇: 第四次