引言
相對於棧而言,堆這片內存面臨着一個稍微復雜的行為模式:在任意時刻,程序可能發出請求,要么申請一段內存,要么釋放一段已經申請過的內存,而且申請的大小從幾個字節到幾個GB都有可能,我們不能假設程序一次申請多少堆空間,因此,堆的管理顯得較為復雜。
那么,使用 malloc() 在堆上分配內存到底是如何實現的呢?
一種做法是把 malloc() 的內存管理交給系統內核去做,既然內核管理着進程的地址空間,那么如果它提供一個系統調用,可以讓 malloc() 使用這個系統調用去申請內存,不就可以了嗎?當然這是一種理論上的做法,但實際上這樣做的性能比較差,因為每次程序申請或者釋放堆空間都要進行系統調用。我們知道系統調用的性能開銷是比較大的,當程序對堆的操作比較頻繁時,這樣做的結果會嚴重影響程序的性能。
比較好的做法就是 malloc() 向操作系統申請一塊適當大小的堆空間,然后由 malloc() 自己管理這塊空間。
malloc() 相當於向操作系統“批發”了一塊較大的內存空間,然后“零售”給程序用。當全部“售完”或程序有大量的內存需求時,再根據實際需求向操作系統“進貨”。當然 malloc() 在向程序零售堆空間時,必須管理它批發來的堆空間,不能把同一塊地址出售兩次,導致地址的沖突。於是 malloc() 需要一個算法來管理堆空間,這個算法就是堆的分配算法。
malloc()和free()的分配算法
在程序運行過程中,堆內存從低地址向高地址連續分配,隨着內存的釋放,會出現不連續的空閑區域,如下圖所示:

帶陰影的方框是已被分配的內存,白色方框是空閑內存或已被釋放的內存。程序需要內存時,malloc() 首先遍歷空閑區域,看是否有大小合適的內存塊,如果有,就分配,如果沒有,就向操作系統申請(發生系統調用)。為了保證分配給程序的內存的連續性,malloc() 只會在一個空閑區域中分配,而不能將多個空閑區域聯合起來。
內存塊(包括已分配和空閑的)的結構類似於鏈表,它們之間通過指針連接在一起。在實際應用中,一個內存塊的結構如下圖所示:

next 是指針,指向下一個內存塊,used 用來表示當前內存塊是否已被使用。這樣,整個堆區就會形成如下圖所示的鏈表:

現在假設需要為程序分配100個字節的內存,當搜索到圖中第一個空閑區域(大小為200個字節)時,發現滿足條件,那么就在這里分配。這時候 malloc() 會把第一個空閑區域拆分成兩部分,一部分交給程序使用,剩下的部分任然空閑,如下圖所示:

仍然以圖3為例,當程序釋放掉第三個內存塊時,就會形成新的空閑區域,free() 會將第二、三、四個連續的空閑區域合並為一個,如下圖所示:

可以看到,malloc() 和 free() 所做的工作主要是對已有內存塊的分拆和合並,並沒有頻繁地向操作系統申請內存,這大大提高了內存分配的效率。
另外,由於單向鏈表只能向一個方向搜索,在合並或拆分內存塊時不方便,所以大部分 malloc() 實現都會在內存塊中增加一個 pre 指針指向上一個內存塊,構成雙向鏈表,如下圖所示:

鏈表是一種經典的堆內存管理方式,經常被用在教學中,很多C語言教程都會提到“棧內存的分配類似於數據結構中的棧,而堆內存的分配卻類似於數據結構中的鏈表”就是源於此。
鏈表式內存管理雖然思路簡單,容易理解,但存在很多問題,例如:
一旦鏈表中的 pre 或 next 指針被破壞,整個堆就無法工作,而這些數據恰恰很容易被越界讀寫所接觸到。
小的空閑區域往往不容易再次分配,形成很多內存碎片。
經常分配和釋放內存會造成鏈表過長,增加遍歷的時間。
針對鏈表的缺點,后來人們提出了位圖和對象池的管理方式,而現在的 malloc() 往往采用多種方式復合而成,不同大小的內存塊往往采用不同的措施,以保證內存分配的安全和效率。
內存池
不管具體的分配算法是怎樣的,為了減少系統調用,減少物理內存碎片,malloc() 的整體思想是先向操作系統申請一塊大小適當的內存,然后自己管理,這就是內存池(Memory Pool)。
內存池的研究重點不是向操作系統申請內存,而是對已申請到的內存的管理,這涉及到非常復雜的算法,是一個永遠也研究不完的課題,除了C標准庫自帶的 malloc(),還有一些第三方的實現,比如 Goolge 的 tcmalloc 和 jemalloc。
我們知道,C/C++是編譯型語言,沒有內存回收機制,程序員需要自己釋放不需要的內存,這在給程序帶來了很大靈活性的同時,也帶來了不少風險,例如C/C++程序經常會發生內存泄露,程序剛開始運行時占用內存很少,隨着時間的推移,內存使用不斷增加,導致整個計算機運行緩慢。
內存泄露的問題往往難於調試和發現,或者只有在特定條件下才會復現,這給代碼修改帶來了不少障礙。為了提高程序的穩定性和健壯性,后來的 Java、Python、C#、JavaScript、PHP 等使用了虛擬機機制的非編譯型語言都加入了垃圾內存自動回收機制,這樣程序員就不需要管理內存了,系統會自動識別不再使用的內存並把它們釋放掉,避免內存泄露。可以說,這些高級語言在底層都實現了自己的內存池,也即有自己的內存管理機制。
池化技術
在計算機中,有很多使用“池”這種技術的地方,除了內存池,還有連接池、線程池、對象池等。以服務器上的線程池為例,它的主要思想是:先啟動若干數量的線程,讓它們處於睡眠狀態,當接收到客戶端的請求時,喚醒池中某個睡眠的線程,讓它來處理客戶端的請求,當處理完這個請求,線程又進入睡眠狀態。
所謂“池化技術”,就是程序先向系統申請過量的資源,然后自己管理,以備不時之需。之所以要申請過量的資源,是因為每次申請該資源都有較大的開銷,不如提前申請好了,這樣使用時就會變得非常快捷,大大提高程序運行效率。