對於malloc來說,很多人都不陌生。然而,我們對它的瞭解並不是很深,我們常常會用,而不明白其中的原理,從而,很容易造成內存泄漏,內存碎片等問題。這常常讓我們頭痛不已,故而我們需要進一步的去了解它。
首先,什麼事malloc?
在很多人認爲malloc是個關鍵字,但是malloc只是C的標準庫中提供的一個普通函數。
malloc 向系統申請分配指定size個字節的內存空間,返回類型是 void* 類型。
它的返回值是如果分配成功則返回指向被分配內存的指針(此存儲區中的初始值不確定),否則返回空指針NULL。當內存不再使用時,應使用free()函數將內存塊釋放。
我們看看其源碼是如何寫的
void * __cdecl _malloc_base (size_t size) { void *res = NULL; // validate size if (size <= _HEAP_MAXREQ) { for (;;) { // allocate memory block res = _heap_alloc(size); // if successful allocation, return pointer to memory // if new handling turned off altogether, return NULL if (res != NULL) { break; } if (_newmode == 0) { errno = ENOMEM; break; } // call installed new handler if (!_callnewh(size)) break; // new handler was successful -- try to allocate again } } else { _callnewh(size); errno = ENOMEM; return NULL; } RTCCALLBACK(_RTC_Allocate_hook, (res, size, 0)); if (res == NULL) { errno = ENOMEM; } return res; }
這是VS2012中malloc的源碼,我們可見其如何實現的,接下來我們看看free的源代碼
void __cdecl _free_base (void * pBlock) { int retval = 0; if (pBlock == NULL) return; RTCCALLBACK(_RTC_Free_hook, (pBlock, 0)); retval = HeapFree(_crtheap, 0, pBlock); if (retval == 0) { errno = _get_errno_from_oserr(GetLastError()); } }
關於內存方面在《UNIX環境高級編程》中第七章的一段話,是這樣說的:
大多數實現所分配的存儲空間比所要求的要稍大一些,額外的空間用來記錄管理信息——分配塊的長度,指向下一個分配塊的指針等等。這就意味着如果寫過一個已分配區的尾端,則會改寫後一塊的管理信息。這種類型的錯誤是災難性的,但是因爲這種錯誤不會很快就暴露出來,所以也就很難發現。將指向分配塊的指針向後移動也可能會改寫本塊的管理信息。
malloc函數的實質體現在,它有一個將可用的內存塊連接爲一個長長的列表的所謂空閒鏈表。調用malloc函數時,它沿連接表尋找一個大到足以滿足用戶請求所需要的內存塊。然後,將該內存塊一分爲二(一塊的大小與用戶請求的大小相等,另一塊的大小就是剩下的字節)。接下來,將分配給用戶的那塊內存傳給用戶,並將剩下的那塊(如果有的話)返回到連接表上。調用free函數時,它將用戶釋放的內存塊連接到空閒鏈上。到最後,空閒鏈會被切成很多的小內存片段,如果這時用戶申請一個大的內存片段,那麼空閒鏈上可能沒有可以滿足用戶要求的片段了。於是,malloc函數請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各內存片段,對它們進行整理,將相鄰的小空閒塊合併成較大的內存塊。
glibc維護了不止一個不定長的內存塊鏈表,而是好幾個,每一個這種鏈表負責一個大小範圍,這種做法有效減少了分配大內存時的遍歷開銷,類似於哈希的方式,將很大的範圍的數據散列到有限的幾個小的範圍內而不是所有數據都放在一起,雖然最終還是要在小的範圍內查找,但是最起碼省去了很多的開銷,如果只有一個不定長鏈表那麼就要全部遍歷,如果分成3個,就省去了2/3的開銷,總之這個策略十分類似於散列。glibc另外的策略就是不止維護一類空閒鏈表,而是另外再維護一個緩衝鏈表和一個高速緩衝鏈表,在分配的時候首先在高速緩存中查找,失敗之後再在空閒鏈表查找,如果找到的內存塊比較大,那麼將切割之後的剩餘內存塊插入到緩存鏈表,如果空閒鏈表查找失敗那麼就往緩存鏈表中查找. 如果還是沒有合適的空閒塊,就向內存申請比請求數更大的內存塊,然後把剩下的內存放入鏈表中。
在對內存塊進行了 free 調用之後,我們需要做的是諸如將它們標記爲未被使用的等事情,並且,在調用 malloc 時,我們要能夠定位未被使用的內存塊。因此, malloc返回的每塊內存的起始處首先要有這個結構:
這就解釋了,爲什麼在程序中free之後,但是堆的內存還是沒有釋放。