malloc free new delete
malloc/free
malloc工作機制
malloc函式的實質體現在,它有一個將可 用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫 malloc 函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分 為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組)。接下來,將分配給使用者的那塊記憶體傳給使用者,並將剩下的那塊(如果有的話)返回到連 接表上。呼叫 free 函式時,它將使用者釋放的記憶體塊連線到空閒鏈上。到最後,空閒鏈會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段,那麼空 閒鏈上可能沒有可以滿足使用者要求的片段了。於是, malloc 函式請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各記憶體片段,對它們進行整理,將相鄰的小空閒 塊合併成較大的記憶體塊。
分配記憶體,在size的基礎上加sizeof(struct mem_control_block) 個位元組。我們主要使用連線的指標遍歷記憶體來尋找開放的記憶體塊。釋放記憶體,將引數指標回退 sizeof(struct mem_control_block) 個位元組,並將其標記為可用的。
C語言是跨平臺的,最終的記憶體處理都是交給系統API完成。系統會記錄每一塊分配記憶體的地址,大小,釋放情況等等。所以malloc只需傳記憶體大小引數,free只需要傳一個地址的引數就可以了。而且同一個地址不能釋放兩次。
debug
debug版本下malloc需要分配的記憶體會比實際的size
_CrtMemBlockHeader是一個雙向連結串列結構,其定義如下:
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader *pBlockHeaderNext; //下次分配的記憶體塊
struct _CrtMemBlockHeader *pBlockHeaderPrev; //上次分配的記憶體塊
char *szFileName; //分配記憶體程式碼的檔名
int nLine; //
size_tnDataSize; //請求的大小,如例項中的
int nBlockUse; //請求的記憶體型別,如例項中的user型別
long lRequest; //請求id,每次請求都會被記錄
unsigned char gap[nNoMansLandSize]; //4位元組校驗位
} _CrtMemBlockHeader;
使用者請求記憶體前後分別有4位元組的校驗位,分配記憶體後都會被初始化為0xFD。如果這8個位元組被改寫,free時就會觸發斷言失敗。而請求的32位元組會被初始化為0xCC(和棧的初始化一樣)。
系統通過記錄這些資訊就能顯示的給出錯誤。比如越界訪問請求的記憶體在debug下會斷言失敗,release下面則不會,從而這會給程式埋下巨大的隱患。很多在release下偶發的錯誤就是這樣產生的。_CrtMemBlockHeader總共32位元組,加上使用者請求的32位元組及最後4位元組校驗位是68位元組。最終呼叫系統的API請求記憶體。比如Windows下面是HeapAlloc。
如果記憶體分配失敗,malloc不像new那樣可以呼叫new_handler來處理,它直接返回NULL。
free則是對_CrtMemBlockHeader的資訊做清理操作,檢查校驗位等等。最終呼叫系統API釋放記憶體。比如Windows下面是HeapFree。
release
實際分配的記憶體等於請求的記憶體大小。malloc和free只是在系統API之上做了些判斷操作。
記憶體碎片
記憶體分配的原理
從作業系統角度來看,程序分配記憶體有兩種方式,分別由兩個系統呼叫完成:brk和mmap(不考慮共享記憶體)。
1、brk是將資料段(.data)的最高地址指標_edata往高地址推;
2、mmap是在程序的虛擬地址空間中(堆和棧中間,稱為檔案對映區域的地方)找一塊空閒的虛擬記憶體。
這兩種方式分配的都是虛擬記憶體,沒有分配實體記憶體。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,作業系統負責分配實體記憶體,然後建立虛擬記憶體和實體記憶體之間的對映關係。
在標準C庫中,提供了malloc/free函式分配釋放記憶體,這兩個函式底層是由brk,mmap,munmap這些系統呼叫實現的。
下面以一個例子來說明記憶體分配的原理:
情況一:malloc小於128k的記憶體,使用brk分配記憶體,將_edata往高地址推(只分配虛擬空間,不對應實體記憶體(因此沒有初始化),第一次讀/寫資料時,引起核心缺頁中斷,核心才分配對應的實體記憶體,然後虛擬地址空間建立對映關係),如下圖:
1.程序啟動的時候,其(虛擬)記憶體空間的初始佈局如圖1所示。
其中,mmap記憶體對映檔案是在堆和棧的中間(例如libc-2.2.93.so,其它資料檔案等),為了簡單起見,省略了記憶體對映檔案。_edata指標(glibc裡面定義)指向資料段的最高地址。
2. 程序呼叫A=malloc(30K)以後,記憶體空間如圖2:
malloc函式會呼叫brk系統呼叫,將_edata指標往高地址推30K,就完成虛擬記憶體分配。
你可能會問:只要把_edata+30K就完成記憶體分配了?
事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊記憶體現在還是沒有物理頁與之對應的,等到程序第一次讀寫A這塊記憶體的時候,發生缺頁中斷,這個時候,核心才分配A這塊記憶體對應的物理頁。也就是說,如果用malloc分配了A這塊內容,然後從來不訪問它,那麼,A對應的物理頁是不會被分配的。
3. 程序呼叫B=malloc(40K)以後,記憶體空間如圖3。
情況二: malloc大於128k的記憶體,使用mmap分配記憶體,在堆和棧之間找一塊空閒記憶體分配(對應獨立記憶體,而且初始化為0),如下圖:
4、程序呼叫C=malloc(200K)以後,記憶體空間如圖4:
預設情況下,malloc函式分配記憶體,如果請求記憶體大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指標了,而是利用mmap系統呼叫,從堆和棧的中間分配一塊虛擬記憶體。
這樣子做主要是因為brk分配的記憶體需要等到高地址記憶體釋放以後才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是記憶體碎片產生的原因,什麼時候緊縮看下面),而mmap分配的記憶體可以單獨釋放。
當然,還有其它的好處,也有壞處,再具體下去,有興趣的同學可以去看glibc裡面malloc的程式碼了。
5、程序呼叫D=malloc(100K)以後,記憶體空間如圖5;
6、程序呼叫free(C)以後,C對應的虛擬記憶體和實體記憶體一起釋放。
7、程序呼叫free(B)以後,如圖7所示:
B對應的虛擬記憶體和實體記憶體都沒有釋放,因為只有一個_edata指標,如果往回推,那麼D這塊記憶體怎麼辦呢?
當然,B這塊記憶體,是可以重用的,如果這個時候再來一個40K的請求,那麼malloc很可能就把B這塊記憶體返回回去了。
8、程序呼叫free(D)以後,如圖8所示:
B和D連線起來,變成一塊140K的空閒記憶體。
9、預設情況下:
當最高地址空間的空閒記憶體超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行記憶體緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閒記憶體超過128K,於是記憶體緊縮
malloc/free和new/delete
malloc 和 new 至少有兩個不同 : new 返回指定型別的指標,並且可以自動計算所需要大小。malloc 只管分配記憶體,並不能對所得的記憶體進行初始化,所以得到的一片新記憶體中,其值將是隨機的。malloc 與 free 是 C++/C 語言的標準庫函式, new/delete 是 C++ 的運算子。它們都可 用於申請動態記憶體和釋放記憶體。 對於非內部資料型別的物件而言,光用maloc/free 無法滿足動態物件的要求。物件 在建立的同時要自動執行建構函式, 物件在消亡之前要自動執行解構函式。由於 malloc/free 是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構函式和解構函式的任務強加於malloc/free 。 因此C++ 語言需要一個能完成動態記憶體分配和初始化工作的運算子 new ,以及一個 能完成清理與釋放記憶體工作的運算子delete 。注意 new/delete 不是庫函式。如果沒有足夠記憶體空間而導致malloc申請失敗,返回NULL。而new則丟擲std::bad_alloc標準異常,不返回NULL。
malloc和new的區別
1)new 返回指定型別的指標,並且可以自動計算所需要大小。而 malloc 則必須要由我們計算位元組數,並且在返回後強行轉換為實際型別的指標。
2)malloc 只管分配記憶體,並不能對所得的記憶體進行初始化,所以得到的一片新記憶體中,其值將是隨機的。除了分配及最後釋放的方法不一樣以外,通過malloc或new得到指標,在其它操作上保持一致。
有了malloc/free為什麼還要new/delete?
1) malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。它們都可用於申請動態記憶體和釋放記憶體。
2) 對於非內部資料型別的物件而言,光用maloc/free無法滿足動態物件的要求。物件在建立的同時要自動執行建構函式,物件在消亡之前要自動執行解構函式。由於malloc/free是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構函式和解構函式的任務強加於malloc/free。
因此C++語言需要一個能完成動態記憶體分配和初始化工作的運算子new,以及一個能完成清理與釋放記憶體工作的運算子delete。注意new/delete不是庫函式。
我們不要企圖用malloc/free來完成動態物件的記憶體管理,應該用new/delete。由於內部資料型別的“物件”沒有構造與析構的過程,對它們而言malloc/free和new/delete是等價的。
既然new/delete的功能完全覆蓋了malloc/free,為什麼C++不把malloc/free淘汰出局呢?
這是因為C++程式經常要呼叫C函式,而C程式只能用malloc/free管理動態記憶體。