內存是承載程序運行的介質,也是程序進行各類運算和表達的場所。算法
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能夠用來定位函數活動記錄中的各個數據。windows
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命中率高的優點,也會浪費必定的空間。
對象池
思路:若是每次分配的空間大小都同樣,那麼就能夠按照這個每次請求分配的大小做爲一個單位,把整個堆空間劃分爲大量的小塊,每次請求的時候,只須要找到一個小塊就能夠了。
對象池的管理方法能夠採用空閒鏈表,也能夠採用位圖,與它們的區別僅僅在於它假定了每次請求的都是一個固定大小,所以實現起來很容易。因爲每次老是隻請求一個單位內存,所以請求獲得知足的速度很是塊,無須查找一個足夠大的空間