一、為什麼需要記憶體池
記憶體是非常寶貴的資源,需要最優通路;
作業系統适合管理大塊記憶體,如一頁(4096位元組),不适合小塊記憶體配置設定;不做記憶體池管理,容易産生記憶體碎片,會出現剩餘記憶體夠,但沒有一塊連續記憶體來配置設定,會引起作業系統把程式HOLD住來整理碎片的情況;
另外直接調用作業系統配置設定記憶體會導緻從使用者态切換到核心态,開銷比較大;
二、記憶體池設計目标:
1、化零為整,減少系統調用;
2、不出現記憶體洩露;
3、高效,盡量無鎖設計;
三、PHP記憶體池實作

這是官方的示意圖,其中free_buckets代表小塊記憶體清單,large_free_buckets代表大塊記憶體清單,還有一個rest_buckets, 鳥哥的解釋是:"這個結構是個雙向清單, 用來儲存一些PHP配置設定後剩下的記憶體, 避免無意義的把剩餘記憶體插入free_buckets帶來的性能問題"。
對于小塊記憶體, PHP還引入了cache機制:
引入cache機制希望做到,一次定位就能查找配置設定。
其中free_bitmap和和large_free_bitmap為位圖,訓示對應位相應的記憶體索引是否有空閑記憶體。
下面會具體說明PHP是如何管理記憶體,在說明之前先說明下環境,筆記實驗的機器是64位的,PHP版本為5.6.2,下面的資料都是基于這個前提。
PHP記憶體管理主要是圍繞free_buckets和large_free_buckets這二個數組來 展開的,這二個數組都是一個長度為64的數組。
1、小塊記憶體管理
free_buckets管理長度小于等于528位元組的記憶體,free_buckets[0]管理長度為長度16-23位元組的記憶體,free_buckets[1]管理長度為24-31位元組的記憶體,依此類推……
其中每一項又是一個雙向連結清單,講起來比較抽象,我們來畫圖描述下配置設定和釋放記憶體後記憶體的布局吧。
剛開始free_buckets數組每項都為NULL:
歸還32位元組記憶體後
歸還36位元組記憶體後
下次假設要配置設定長度32-39位元組之間的記憶體如35,直接從下标2中周遊元素,隻要哪個元素的長度大于等于要配置設定的長度,即将長度為36的記憶體歸還。
接下來我們看下小塊記憶體的配置設定是怎麼處理的,為了保證記憶體配置設定的高效,PHP每次會從作業系統配置設定大塊記憶體,預設是256KB,可以通過環境變量ZEND_MM_SEG_SIZE來設定。
從作業系統配置設定記憶體後,PHP會根據前面的換算關系,将記憶體塊放到相應的記憶體塊中,便于後續快速配置設定。
2、大塊記憶體管理
小塊記憶體管理長度小于等于528(參考宏ZEND_MM_MAX_SMALL_SIZE的定義)位元組的記憶體,大于528的都由large_free_buckets來管理,large_free_buckets也是長度為64的數組,每個下标管理的記憶體範圍是前開閉區間,設下标為i,則管理記憶體長度為[2^i, 2^(i+1))。
舉幾個例子,large_free_buckets[9]的下标為9,2的9次方是512, 是以其管理長度為512-1023之間的記憶體;
large_free_buckets[10]管理長度為1024-2047之間的記憶體;large_free_buckets[11]管理長度為2048-4095之間的記憶體……
這樣一共可以管理最大2^64的記憶體,當然實際不會用這麼多,因為PHP有記憶體限制相關參數。
可以看到,在大塊記憶體的設計時,并沒有和小塊記憶體一樣每個下标管理的記憶體長度差為8,而是下一個下标管理的長度為上一個下标管理的長度的2倍;之是以這樣設計,因為大塊記憶體比較大,不用太細的管理,另外就是要盡量節省記憶體,如果相鄰下标管理記憶體長度差為8位元組,則需要很大的數組來管理這些記憶體。
這樣設計還會有個問題,可能會造成巨大的記憶體浪費,如下标10管理1024-2047之間的記憶體,如果釋放一塊長度為2046的記憶體,但申請的時候隻要1030位元組,則多餘的1016位元組就白白浪費了,對于這個問題,PHP通過樹和雙向清單來管理:
什麼意思呢,結合釋放記憶體的過程說明下:
1)、釋放2048位元組記憶體
結合前面講的,落在下标的11元素上,2048的二進制是100000000000,其中第1個1表示落在哪個下标中,這裡從右到左數排第12位,從0開始計算就是第11位;
從左到右數第二位是0,是以放到下标1這個元素的左子樹上。
2、釋放3100位元組記憶體
3100的二進制是110000011100,從左到右數第二位是1,是以放在右子樹上。
3、釋放4093位元組記憶體
4093的二進制是111111111101,從左到右第二位是1,放右子樹上,發現右子樹已經有了3100,再往右數,第三位還是1,是以放到3100的右子樹上
申請的時候就是掃這個順序掃描的,如果對應二進制位為0,則掃描左子樹,如果為1則掃描右子樹。
舉個例子,這時如果要申請2900位元組記憶體,轉成二進制為101101010100,從左往右第2位為1,是以走到3100這裡就傳回了,而不會配置設定到4093位元組的記憶體。
四、總結
1、PHP的記憶體配置設定主要是圍繞兩個數組來展開:free_buckets和large_free_buckets,其中前者用來管理小塊記憶體,後者用來管理大塊記憶體。
2、對于小塊記憶體,做到盡量可以再次使用,分成64個區段,每段管理的記憶體位元組間隔為8,即下标為0管理16-23,下标1管理24-31,依此類推……
3、對于大塊記憶體,數組不宜過大,是以數組的長度也是64,但是為了不浪費記憶體,采用樹+雙向清單來管理,友善快速查找,也不會浪費太多記憶體。
4、記憶體配置設定時先從作業系統配置設定較大塊記憶體,配置設定完後放入上述相應的數組中,友善下次使用。