天天看點

C++程式記憶體結構C++程式記憶體結構

C++程式記憶體結構

标簽(空格分隔): C++ 記憶體

  1. 棧區(stack):又編譯器自動配置設定釋放,存放函數的參數值,局部變量的值等,其操作方式類似于資料結構的棧。
  2. 堆區(heap):一般是由程式員配置設定釋放,若程式員不釋放的話,程式結束時可能由OS回收,值得注意的是他與資料結構的堆是兩回事,配置設定方式倒是類似于資料結構的連結清單。
  3. 未初始化資料區(BSS):未初始化或初值為0的全局變量和靜态局部變量。
  4. 資料區(data segment):已初始化且初值非0的全局變量和靜态局部變量
  5. 程式代碼區:可執行代碼、字元串字面值、隻讀變量二進制代碼。

下圖便是linux系統一個程序的記憶體占用示意圖:

C++程式記憶體結構C++程式記憶體結構

1.從低位址到高位址分别為:代碼段、(初始化)資料段、(未初始化)資料段(BSS)、堆、棧、指令行參數和環境變量

2.堆向高記憶體位址生長  

3.棧向低記憶體位址生長

其中,使用者位址空間中的藍色條帶對應于映射到實體記憶體的不同記憶體段,灰白區域表示未映射的部分。這些段隻是簡單的記憶體位址範圍,與Intel處理器的段沒有關系。

上圖中Random stack offset和Random mmap offset等随機值意在防止惡意程式。Linux通過對棧、記憶體映射段、堆的起始位址加上随機偏移量來打亂布局,以免惡意程式通過計算通路棧、庫函數等 位址。execve(2)負責為程序代碼段和資料段建立映射,真正将代碼段和資料段的内容讀入記憶體是由系統的缺頁異常處理程式按需完成的。另 外,execve(2)還會将BSS段清零。

1 核心空間

核心總是駐留在記憶體中,是作業系統的一部分。核心空間為核心保留,不允許應用程式讀寫該區域的内容或直接調用核心代碼定義的函數。
           

2 棧(stack)

棧又稱堆棧,由編譯器自動配置設定釋放,行為類似資料結構中的棧(先進後出)。堆棧主要有三個用途:

為函數内部聲明的非靜态局部變量(C語言中稱“自動變量”)提供存儲空間。記錄函數調用過程相關的維護性資訊,稱為棧幀(Stack Frame)或過程活動記錄(Procedure Activation Record)。它包括函數傳回位址,不适合裝入寄存器的函數參數及一些寄存器值的儲存。除遞歸調用外,堆棧并非必需。因為編譯時可獲知局部變量,參數和傳回位址所需空間,并将其配置設定于BSS段。
 臨時存儲區,用于暫存長算術表達式部分計算結果或alloca()函數配置設定的棧内記憶體。

 持續地重用棧空間有助于使活躍的棧記憶體保持在CPU緩存中,進而加速通路。程序中的每個線程都有屬于自己的棧。向棧中不斷壓入資料時,若超出其容量就會 耗盡棧對應的記憶體區域。這将觸發一個頁錯誤。此時若棧的大小低于堆棧最大值RLIMIT_STACK(通常是8M),則棧會動态增長,程式繼續運作。映射 的棧區擴充到所需大小後,不會再收縮回去。

 Linux中ulimit -s指令可檢視和設定堆棧最大值,當程式使用的堆棧超過該值時, 發生棧溢出(Stack Overflow),程式收到一個段錯誤(Segmentation Fault)。注意,調高堆棧容量可能會增加記憶體開銷和啟動時間。

 堆棧既可向下增長(向記憶體低位址)也可向上增長, 這依賴于具體的實作。本文所述堆棧向下增長。

 堆棧的大小在運作時由核心動态調整。
           

3 記憶體映射段(mmap)

此處,核心将硬碟檔案的内容直接映射到記憶體,任何應用程式都可通過Linux的mmap()系統調用或Windows的CreateFileMapping()/MapViewOfFile()請求這種映射。記憶體映射是一種友善高效的檔案I/O方式,因而被用于裝載動态共享庫。使用者也可建立匿名記憶體映射,該映射沒有對應的檔案,可用于存放程式資料。在Linux中,若通過malloc()請求一大塊記憶體,C運作庫将建立一個匿名記憶體映射,而不使用堆記憶體。”大塊”意味着比門檻值MMAP_THRESHOLD還大,預設為128KB,可通過mallopt()調整。

 該區域用于映射可執行檔案用到的動态連結庫。在Linux 2.4版本中,若可執行檔案依賴共享庫,則系統會為這些動态庫在從0x40000000開始的位址配置設定相應空間,并在程式裝載時将其載入到該空間。在 Linux 2.6核心中,共享庫的起始位址被往上移動至更靠近棧區的位置。

 從程序位址空間的布局可以看到,在有共享庫的情況下,留給堆的可用空間還有兩處:一處是從.bss段到0x40000000,約不到1GB的空間;另一處是從共享庫到棧之間的空間,約不到2GB。這兩塊空間大小取決于棧、共享庫的大小和數量。這樣來看,是否應用程式可申請的最大堆空間隻有2GB?事實 上,這與Linux核心版本有關。在上面給出的程序位址空間經典布局圖中,共享庫的裝載位址為0x40000000,這實際上是Linux kernel 2.6版本之前的情況了,在2.6版本裡,共享庫的裝載位址已經被挪到靠近棧的位置,即位于0xBFxxxxxx附近,是以,此時的堆範圍就不會被共享庫 分割成2個“碎片”,故kernel 2.6的32位Linux系統中,malloc申請的最大記憶體理論值在2.9GB左右。
           

4 堆(heap)

堆用于存放程序運作時動态配置設定的記憶體段,可動态擴張或縮減。堆中内容是匿名的,不能按名字直接通路,隻能通過指針間接通路。當程序調用malloc(C)/new(C++)等函數配置設定記憶體時,新配置設定的記憶體動态添加到堆上(擴張);當調用free(C)/delete(C++)等函數釋放 記憶體時,被釋放的記憶體從堆中剔除(縮減) 。

 配置設定的堆記憶體是經過位元組對齊的空間,以适合原子操作。堆管理器通過連結清單管理每個申請的記憶體,由于堆申請和釋放是無序的,最終會産生記憶體碎片。堆記憶體一般由應用程式配置設定釋放,回收的記憶體可供重新使用。若程式員不釋放,程式結束時作業系統可能會自動回收。

 堆的末端由break指針辨別,當堆管理器需要更多記憶體時,可通過系統調用brk()和sbrk()來移動break指針以擴張堆,一般由系統自動調用。

 使用堆時經常出現兩種問題:
 1) 釋放或改寫仍在使用的記憶體(“記憶體破壞”);
 2)未釋放不再使用的記憶體(“記憶體洩漏”)。當釋放次數少于申請次數時,可能已造成記憶體洩漏。洩漏的記憶體往往比忘記釋放的資料結構更大,因為所配置設定的記憶體通常會圓整為下個大于申請數量的2的幂次(如申請212B,會圓整為256B)。

 注意,堆不同于資料結構中的”堆”,其行為類似連結清單。
           

在實際程式設計中會遇到的與記憶體空間相關的問題

1.記憶體申請:

為了解決資料存儲的問題,我們有3種辦法申請空間并使用它們:

第一,從棧空間中申請(即直接定義數組)

第二,從堆空間中申請(使用malloc或 者new動态申請記憶體)

第三,使用檔案存儲資料  

我們主要讨論前兩個方案  

2、申請後系統的響應: 

棧:隻要棧的剩餘空間大于所申請的空間,系統将為程式提供記憶體,否則将報異常提示棧溢出。

堆:首先應該知道作業系統有一個記錄記憶體位址的連結清單,當系統收到程式的申請時,會周遊該連結清單,尋找第一個空間大于所申請的空間的堆結點,然後将該結點從空閑結點連結清單中删除,并将該結點的空間配置設定給程式。另外,對于大多數系統,會在這塊記憶體空間中的首位址處記錄本次配置設定的大小,這樣代碼中的delete或free語句就能夠正确的釋放本記憶體空間。另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會将多餘的那部分重新放入空閑連結清單中。

3、申請的大小限制不同:

棧:在windows下,棧是向低位址擴充的資料結構,是一塊連續的記憶體區域,棧 頂的位址和棧的最大容量是系統預先規定好的,能從棧獲得的空間較小。

堆:堆是向高位址擴充的資料結構,是不連續的記憶體區域,這是由于系統是由連結清單在存儲空閑記憶體位址,自然堆就是不連續的記憶體區域,且連結清單的周遊也是從低位址向高位址周遊的,堆得大小受限于計算機系統的有效虛拟記憶體空間,由此空間,堆獲得的空間比較靈活,也比較大。

4、申請的效率不同:

棧:棧由系統自動配置設定,速度快,但是程式員無法控制。

堆:堆是有程式員自己配置設定,速度較慢,容易産生碎片,不過用起來友善。

5、堆和棧的存儲内容不同:

棧:在函數調用時,第一個進棧的是主函數中函數調用後的下一條指令的位址,然後是函數的各個參數,在大多數的C編譯器中,參數是從右往左入棧的,當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的位址,也就是主函數中的下一條指令。

堆:一般是在堆得頭部用一個位元組存放堆得大小,具體内容由程式員安排。

總結  

資料量較小時,推薦使用棧空間申請,即直接定義數組;資料量稍大或者不确定時,推薦使用堆空間記憶體,即使用malloc或者new動态申請,因 為棧空間常常會有大小的限定,當棧空間耗盡時,棧溢出會導緻程式崩潰  當資料量超大的,建議重新審閱算法或者使用檔案存儲棧空間與子函數,遞歸與棧溢出當一個子函數被調用時,子函數的資料及代碼都會被裝入棧中,因為棧空間通常會有大小限制,如果子函數太多時,就會有棧溢出的風險。是以當程式員考 慮使用遞歸函數解決問題時,應當考慮到棧溢出的風險。

繼續閱讀