php 剩余空间,PHP内核探索:内存的申请与销毁
PHP底層對內存的管理, 圍繞著小塊內存列表(free_buckets)、 大塊內存列表(large_free_buckets)和 剩余內存列表(rest_buckets)三個列表來分層進行的。 ZendMM向系統進行的內存申請,并不是有需要時向系統即時申請, 而是由ZendMM的最底層(heap層)先向系統申請一大塊的內存,通過對上面三種列表的填充, 建立一個類似于內存池的管理機制。 在程序運行需要使用內存的時候,ZendMM會在內存池中分配相應的內存供使用。 這樣做的好處是避免了PHP向系統頻繁的內存申請操作,如下面的代碼:
$nowamagic = "o_o\n";
echo $nowamagic;
?>
這是一個簡單的php程序,但通過對emalloc的調用計數,發現對內存的請求有數百次之多, 當然這非常容易解釋,因為PHP腳本的執行,需要大量的環境變量以及內部變量的定義, 這些定義本身都是需要在內存中進行存儲的。
在編寫PHP的擴展時,推薦使用emalloc來代替malloc,其實也就是使用PHP的ZendMM來代替 手動直接調用系統級的內存管理。(除非,你自己知道自已在做什么。)
那么在上面這個小程序的執行過程中,ZendMM是如何使用自身的heap層存儲空間的呢? 經過對源碼的追蹤我們可以找到:
ZEND_ASSIGN_SPEC_CV_CONST_HANDLER (......)
-> ALLOC_ZVAL(......)
-> ZEND_FAST_ALLOC(......)
-> emalloc (......)
-> _emalloc(......)
-> _zend_mm_alloc_int(.....)
void *_emalloc 實現了對內存的申請操作,在_emalloc的處理過程中, 對是否使用ZendMM進行了判斷,如果heap層沒有使用ZendMM來管理, 就直接使用_zend_mm_heap結構中定義的_malloc函數進行內存的分配; (我們通過上節可以知道,這里的_malloc可以是malloc,win32,mmap_anon,mmap_zero中的一種)。
就目前所知,不使用ZendMM進行內存管理,唯一的用途是打開enable-debug開關后, 可以更方便的追蹤內存的使用情況。所以,在這里我們關注ZendMM使用_zend_mm_alloc_int函數進行內存分配:
PHP內存管理器
結合上圖,再加上內存分配之前的驗證,ZendMM對內存分配的處理主要有以下步驟:
內存檢查。 對要申請的內存大小進行檢查,如果太大(超出memory_limit則報 Out of Memory);
如果命中緩存,使用fastcache得到內存塊(詳見第五節),然后直接進行第5步;
在ZendMM管理的heap層存儲中搜索合適大小的內存塊, 在這一步驟ZendMM通過與ZEND_MM_MAX_SMALL_SIZE進行大小比較, 把內存請求分為兩種類型: large和small。small類型的的請求會先使用zend_mm_low_bit函數 在mm_heap中的free_buckets中查找,未找到則使用與large類型相同的方式: 使用zend_mm_search_large_block函數在“大塊”內存(_zend_mm_heap->large_free_buckets)中進行查找。 如果還沒有可以滿足大小需求的內存,最后在rest_buckets中進行查找。 也就是說,內存的分配是在三種列表中小到大進行的。 找到可以使用的block后,進行第5步;
如果經過第3步的查找還沒有找到可以使用的資源(請求的內存過大),需要使用ZEND_MM_STORAGE_ALLOC函數向系統再申請一塊內存(大小至少為ZEND_MM_SEG_SIZE),然后直接將對齊后的地址分配給本次請求。跳到第6步;
使用zend_mm_remove_from_free_list函數將已經使用block節點在zend_mm_free_block中移除;
內存分配完畢,對zend_mm_heap結構中的各種標識型變量進行維護,包括large_free_buckets, peak,size等;
返回分配的內存地址;
從上面的分配可以看出,PHP對內存的分配,是結合PHP的用途來設計的,PHP一般用于web應用程序的數據支持, 單個腳本的運行周期一般比較短(最多達到秒級),內存大塊整塊的申請,自主進行小塊的分配, 沒有進行比較復雜的不相臨地址的空閑內存合并,而是集中再次向系統請求。 這樣做的好處就是運行速度會更快,缺點是隨著程序的運行時間的變長, 內存的使用情況會“越來越多”(PHP5.2及更早版本)。 所以PHP5.3之前的版本并不適合做為守護進程長期運行。 (當然,可以有其他方法解決,而且在PHP5.3中引入了新的GC機制,詳見下一小節)
內存的銷毀
ZendMM在內存銷毀的處理上采用與內存申請相同的策略,當程序unset一個變量或者是其他的釋放行為時, ZendMM并不會直接立刻將內存交回給系統,而是只在自身維護的內存池中將其重新標識為可用, 按照內存的大小整理到上面所說的三種列表(small,large,free)之中,以備下次內存申請時使用。
內存銷毀的最終實現函數是_efree。在_efree中,內存的銷毀首先要進行是否放回cache的判斷。 如果內存的大小滿足ZEND_MM_SMALL_SIZE并且cache還沒有超過系統設置的ZEND_MM_CACHE_SIZE, 那么,當前內存塊zend_mm_block就會被放回mm_heap->cache中。 如果內存塊沒有被放回cache,則使用下面的代碼進行處理:
zend_mm_block *mm_block; //要銷毀的內存塊
zend_mm_block *next_block;
...
next_block = ZEND_MM_BLOCK_AT(mm_block, size);
if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
zend_mm_remove_from_free_list(heap, (zend_mm_free_block *) next_block);
size += ZEND_MM_FREE_BLOCK_SIZE(next_block);
}
if (ZEND_MM_PREV_BLOCK_IS_FREE(mm_block)) {
mm_block = ZEND_MM_PREV_BLOCK(mm_block);
zend_mm_remove_from_free_list(heap, (zend_mm_free_block *) mm_block);
size += ZEND_MM_FREE_BLOCK_SIZE(mm_block);
}
if (ZEND_MM_IS_FIRST_BLOCK(mm_block) &&
ZEND_MM_IS_GUARD_BLOCK(ZEND_MM_BLOCK_AT(mm_block, size))) {
zend_mm_del_segment(heap, (zend_mm_segment *) ((char *)mm_block - ZEND_MM_ALIGNED_SEGMENT_SIZE));
} else {
ZEND_MM_BLOCK(mm_block, ZEND_MM_FREE_BLOCK, size);
zend_mm_add_to_free_list(heap, (zend_mm_free_block *) mm_block);
}
這段代碼邏輯比較清晰,主要是根據當前要銷毀的內存塊mm_block在zend_mm_heap 雙向鏈表中所處的位置進行不同的操作。如果下一個節點還是free的內存,則將下一個節點合并; 如果上一相鄰節點內存塊為free,則合并到上一個節點; 如果只是普通節點,剛使用 zend_mm_add_to_free_list或者zend_mm_del_segment 進行回收。
就這樣,ZendMM將內存塊以整理收回到zend_mm_heap的方式,回收到內存池中。 程序使用的所有內存,將在進程結束時統一交還給系統。在內存的銷毀過程中,還涉及到引用計數和垃圾回收(GC),將在下一小節進行討論。
延伸閱讀
此文章所在專題列表如下:
總結
以上是生活随笔為你收集整理的php 剩余空间,PHP内核探索:内存的申请与销毁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sentry php使用,实战Sentr
- 下一篇: java正则hitend,Java Ma