“text segment ”是應用程序運行時應用程序代碼存在的內存段。每一個指令,每一個單個函數、過程、方法和執行代碼都存在這個內存段中直到應用程序退出。一般情況下,你不會真的不得不知道這個段的任何事情。
當應用開始以后,函數main() 被調用,一些空間分配在”stack” 中。這是為應用分配的另一個段的內存空間,這是為了函數變量存儲需要而分配的內存。每一次在應用中調用一個函數,“stack ”的一部分會被分配在”stack” 中,稱之為”frame” 。新函數的本地變量分配在這里。
正如名稱所示,“stack ”是后進先出(LIFO )結構。當函數調用其他的函數時,“stack frame ”會被創建;當其他函數退出后,這個“frame ”會自動被破壞。
“heap” 段也稱為”data” 段,提供一個保存中介貫穿函數的執行過程,全局和靜態變量保存在“heap ”中,直到應用退出。
為了訪問你創建在heap 中的數據,你最少要求有一個保存在stack 中的指針,因為你的CPU 通過stack 中的指針訪問heap 中的數據。
你可以認為stack 中的一個指針僅僅是一個整型變量,保存了heap 中特定內存地址的數據。實際上,它有一點點復雜,但這是它的基本結構。
簡而言之,操作系統使用stack 段中的指針值訪問heap 段中的對象。如果stack 對象的指針沒有了,則heap 中的對象就不能訪問。這也是內存泄露的原因。
在iOS 操作系統的stack 段和heap 段中,你都可以創建數據對象。
stack 對象的優點主要有兩點,一是創建速度快,二是管理簡單,它有嚴格的生命周期。stack 對象的缺點是它不靈活。創建時長度是多大就一直是多大,創建時是哪個函數創建的,它的owner 就一直是它。不像heap 對象那樣有多個owner ,其實多個owner 等同于引用計數。只有heap 對象才是采用“引用計數”方法管理它。
stack 對象的創建
只要棧的剩余空間大于stack 對象申請創建的空間,操作系統就會為程序提供這段內存空間,否則將報異常提示棧溢出。
heap 對象的創建
操作系統對于內存heap 段是采用鏈表進行管理的。操作系統有一個記錄空閑內存地址的鏈表,當收到程序的申請時,會遍歷鏈表,尋找第一個空間大于所申請的heap 節點,然后將該節點從空閑節點鏈表中刪除,并將該節點的空間分配給程序。
例如:
NSString 的對象就是stack 中的對象,NSMutableString 的對象就是heap 中的對象。前者創建時分配的內存長度固定且不可修改;后者是分配內存長度是可變的,可有多個owner, 適用于計數管理內存管理模式。
兩類對象的創建方法也不同,前者直接創建“NSString * str1=@"welcome"; “,而后者需要先分配再初始化“ NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"]; ”。
再補充一點,這里說的是操作系統的堆和棧。
在我們學習“數據結構”時,接觸到的堆和棧的概念和這個操作系統中的堆和棧不是一回事的。
操作系統的堆和棧是指對內存進行操作和管理的一些方式。
“數據結構“的堆實際上指的就是(滿足堆性質的)優先Queue 的一種數據結構,第1 個元素有最高的優先權;棧實際上就是滿足先進后出的性質的數據或數據結構。
一、堆和棧的概念區別
堆: 是大家共有的空間,分全局堆和局部堆。全局堆就是所有沒有分配的空間,局部堆就是用戶分配的空間。堆在操作系統對進程 初始化的時候分配,運行過程中也可以向系統要額外的堆,但是記得用完了要還給操作系統,要不然就是內存泄漏。堆里面一般 放的是靜態數據,比如static的數據和字符串常量等,資源加載后一般也放在堆里面。一個進程的所有線程共有這些堆 ,所以對堆的操作要考慮同步和互斥的問題。程序里面編譯后的數據段都是堆的一部分。
棧: 是個線程獨有的,保存其運行狀態和局部自動變量的。棧在線程開始的時候初始化,每個線程的棧互相獨立,因此 ,棧是 thread safe的。每個c++對象的數據成員也存在在棧中,每個函數都有自己的棧,棧被用來在函數之間傳遞參數。操作系統在切換線程的時候會自動的切換棧,就是 切換ss/esp寄存器。棧空間不需要在高級語言里面顯式的分配 和釋放。
預備知識—程序的內存分配
一個由c/C++編譯的程序占用的內存分為以下幾個部分:
1、棧區(stack)— 由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。
2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表。
3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束后由系統釋放。
4、文字常量區 —常量字符串就是放在這里的。程序結束后由系統釋放。
5、程序代碼區—存放函數體的二進制代碼。
二、例子程序
//main.cpp
int a = 0; 全局初始化區
char *p1; 全局未初始化區
main()
{
int b; //棧
char s[] = "abc"; //棧
char *p2; //棧
char *p3 = "123456"; //123456{row.content}在常量區,p3在棧上。
static int c =0; //全局(靜態)初始化區
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得來的10和20字節的區域就在堆區。
strcpy(p1, "123456"); //123456{row.content}放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。
}
三、堆和棧的理論知識
1.申請方式
stack:由系統自動分配。例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間
heap:需要程序員自己申請,并指明大小,在c中malloc函數
如p1 = (char *)malloc(10);
在C++中用new運算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在棧中的。
2.申請后系統的響應
棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
堆:首 先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結 點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才 能正確的釋放本內存空間。另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。
3.申請大小的限制
棧: 在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也可能是1M,它是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能 從棧獲得的空間較小。堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈
表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
4.申請效率的比較:
棧由系統自動分配,速度較快。但程序員是無法控制的。棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。堆則是C/C++函數庫提供的,它的機制是很復雜的
5.堆和棧中的存儲內容
棧: 在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排
6.存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
比如:
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小結:
堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,吃完之后還得清洗、打掃等后續工作(不然用完了不清洗就不好再用),但是比較符合自己的口味,而且自由度大。
管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產生memory leak。
申請大小:
棧:在Windows下,棧是向低地址擴展的數據結構,是一 塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因 此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
碎片問題:對于堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對于棧來講,則不會存在這個問題,因為棧是先進后出的隊列,他們是如此的一一對應,以至于永遠都不可能有一個內存塊從棧中間彈出
分配方式:堆都是動態分配的,沒有靜態分配的堆。
棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很復雜的。