內存是承載程序運行的介質,也是程序進行各種運算和表達的場所。
10.1 程序的內存布局
現代的應用程序都運行在一個內存空間里,在32位系統里,這個內存空間擁有4GB(2的32次方)的尋址能力。現在的應用程序可以直接使用32位地址進行尋址,這被稱為平坦的內存模型。在平坦的內存模型中,整個內存是一個統一的地址空間,用戶可以使用一個32位的指針訪問任意的內存位置。
大多數操作系統都會將4GB內存空間中的一部分挪給內核使用,應用程序無法直接訪問這一段內存,這一部分內存地址被稱為內核空間。
應用程序使用內存空間有如下”默認”的區域:
- 棧:用於維護函數調用的上下文,離開了棧函數調用就沒辦法實現
- 堆:堆是用來容納應用程序動態分配內存區域,當程序使用malloc或new分配內存時,得到的內存來自堆里。
- 可執行文件映像:這里存儲着可執行文件在內存里的映像,有裝載器在裝載時將可執行文件的內存讀取或映射到這里。
- 保留區:不是一個單一的內存區域,而是對內存中受到保護而禁止訪問的內存區域總稱。
10.2 棧與調用慣例
10.2.1 什么是棧
棧被定義為一個特殊的容器,用戶可以將數據壓入棧中(入棧,push),也可以將已經壓入棧中的數據彈出(出棧,pop),但棧這個容器必須遵守一條規格:先入棧的數據后出棧。
棧是一個具有上面屬性的動態內存區域。壓棧操作使得棧增大,而彈出操作使棧減小。
在經典操作系統中,棧總使向下增長的。
棧保存一個函數調用所需要的維護信息,這常常被稱為堆棧幀或活動記錄。
堆棧幀一般包括如下幾方面:
- 函數返回地址和參數
- 臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其他臨時變量。
- 保存的上下文:包括在函數調用前后需要保持不變的寄存器。
在一個i386下的函數總是這樣調用的:
- 把所有或一部分參數壓入棧中,如果有其他參數沒有入棧,那么使用某些特定的寄存器傳遞。
- 把當前指令的下一條指令的地址壓入棧中。
- 跳轉到函數體執行
一個函數的活動記錄用ebp和esp這兩個寄存器划定范圍。esp寄存器始終指向棧的頂部,同時也就是指向當前函數的活動記錄的頂部。ebp寄存器指向了函數活動記錄的一個固定位置,ebp寄存器又被稱為幀指針。

ebp固定在圖中所示的位置,不隨這個函數的執行而變化。esp始終指向棧頂,因此隨這函數的執行,esp會不斷變化。固定不變的ebp可以用來定位函數活動記錄中的各個數據。
10.2.2 調用慣例
函數的調用方和被調用方對於函數如何調用須要有一個明確的約定,這樣的約定就是調用慣例。
一個調用慣例有如下幾方面:
- 函數參數的傳遞順序和方式:最常見的一種是通過棧傳遞。對於有多個參數的函數,調用慣例要規定函數調用方將參數壓棧的順序:是從左至右,還是從右至左。
- 棧的維護方式:在函數將參數壓棧之后,函數體會被調用,此后需要將壓入棧中的參數全部彈出,以使得棧在函數調用前后保持一致。
- 名字修飾:為了鏈接的時候堆調用慣例進行區分,調用管理要對函數本身的名字進行修飾。
多級調用棧布局:


10.2.3 函數返回值傳遞
除了參數傳遞之外,函數與調用方的交互好友一個渠道就是返回值。eax是傳遞返回值的通道。函數將返回值存儲在eax中,返回后函數的調用方在讀取eax。對於大於4字節的返回值,采用eax和edx聯合返回的方式進行。
如果返回值太大,C語言在函數返回時會使用一個臨時的棧上內存區域作為中轉,結果返回值對象會被拷貝兩次。
10.3 堆和內存管理
堆這片內存面臨一個復雜的行為模式:在任意時刻程序可能發出請求,申請一段內存或者釋放一段已經申請了的內存,而且申請的大小從幾個字節到數GB都可能,我們不能假設程序會一次申請多少空間,所以,堆的管理比較復雜
10.3.1 什么是堆
棧上的數據在函數返回的時候就會被釋放掉,所以無法將數據傳遞至函數外部。全局變量沒有辦法動態產生數據,只能在編譯器的時候定義。這這種情況下,堆是唯一選擇。
堆是一塊巨大的內存空間,常常占據了整個虛擬空間的絕大部分。
10.3.2 Linux進程堆管理
Linux下進程堆管理有兩種分配方式,即兩個系統調用:
- brk()系統調用:實際上就是設置進程數據段的結束地址,它可以擴大或者縮小數據段。
- mmap()系統調用:作用和windows下的VirtualAlloc相似,作用是向操作系統申請一段虛擬地址空間,這塊虛擬地址空間可以映射到某個文件,當它不將地址空間映射到某個文件時,我們稱做為匿名空間,它可以拿來作為堆空間。
10.3.3 Windows進程堆管理
Windows的進程將地址分配給了各種EXE、DLL、堆、棧。
每個線程默認棧的大小是1MB,在線程啟動時,系統就會為它的進程地址空間中分配相應的地址空間作為棧,線程棧的大小可以由創建時CreatThread的參數指定。
Windows提供一個API叫做VirtualAlloc(),用來向系統申請空間,它要求空間大小必須為頁的整數倍。
堆分配算法在的實現位於堆管理器,堆管理器提供了一套與堆相關的API用來創建、分配、釋放和銷毀堆空間
- HeapCreate:創建一個堆
- HeapAlloc:在一個堆中分配內存
- HeapFree:釋放已經分配的內存
- HeapDestroy:摧毀一個堆
每個進程在創建時都會有一個默認堆,這個堆在進程啟動時創建,並且直到進程結束都一直存在。默認堆大小為1MB。一個進程中一次性能夠分配的最大堆空間取決於最大的那個堆。
10.3.4 堆分配算法
如何管理一大塊連續的內存空間,能夠按照需求分配、釋放其中的空間,這就是堆算法。
空間鏈表
空閑鏈表實際上就是把堆中各個空閑的塊按照鏈表的方式連接起來,當用戶請求一塊空間時,可以遍歷整個列表,直到找到合適大小的塊並且將它拆分,當用戶釋放空間時將它合並到空閑鏈表中。
空閑鏈表時這樣一種結構,在堆里的每個空閑空間的大小(或結尾)有一個頭(Header),頭結構里記錄了上一個(prev)和下一個(next)空閑塊的地址。所有的空閑塊形成一個鏈表。

位圖
核心思想:將整個堆划分為大量的塊,每個塊的大小相同。當用戶請求內存的時候,總時分配整數個塊給用戶,第一個塊我們稱為已分配區域的頭,其余的稱為已分配區域的主體。而我們可以使用一個整數數組來記錄塊的使用情況,由於每個塊只有頭/主體/空閑三種狀態,所以僅僅需要2為即可表示一個塊,所以稱為位圖。
優點:
- 速度快:由於整個堆的空閑信息存儲在一個數組內,所以訪問數組時cache容易命中。
- 穩定性好:為了避免用戶越界讀寫破壞數據,我們只需簡單的備份一下位圖即可,而且即使部分數據被破壞,也不會導致整個堆無法工作。
- 塊不需要額外信息,易於管理。
缺點:
- 分配內存時容易產生碎片。
- 如果堆很大,或者設定的一個塊很小,那么位圖將會很大,可能失去cache命中率高的優勢,也會浪費一定的空間。
對象池
思路:如果每次分配的空間大小都一樣,那么就可以按照這個每次請求分配的大小作為一個單位,把整個堆空間划分為大量的小塊,每次請求的時候,只需要找到一個小塊就可以了。
對象池的管理方法可以采用空閑鏈表,也可以采用位圖,與它們的區別僅僅在於它假定了每次請求的都是一個固定大小,因此實現起來很容易。由於每次總是只請求一個單位內存,因此請求得到滿足的速度非常塊,無須查找一個足夠大的空間
