本文是
php-internals
的讀書筆記.
概述
1) 操作系統直接管理著內存,所以操作系統也需要進行內存管理,計算機中通常都有內存管理單元(MMU) 用于處理CPU對內存的訪問。
2) 應用程序無法直接調用物理內存, 只能向系統申請內存。
向操作系統申請內存空間會引發系統調用。
系統調用會將CPU從用戶態切換到內核。
為了減少系統調用開銷。通常在用戶態進行內存管理。 申請大塊內存備用。使用完的內存不馬上釋放,將內存復用,避免多次內存申請和釋放所帶來性能消耗。
3) PHP不需要顯示內存管理,由Zend引擎進行管理。
PHP內存限制
1)php.ini中的默認32MB
memory_limit = 32M
2)動態修改內存
ini_set ("memory_limit", "128M")
3)獲取目前內存占用
memory_get_usage() : 獲取PHP腳本所用的內存大小
memory_get_peak_usage() :返回當前腳本到目前位置所占用的內存峰值。
學習內存管理的目的
了解PHP如何占用內存,可以避免不必要的內存浪費。
PHP中的內存管理###
包含:
1)足夠內存
2)可用內存獲取部分內存
3)使用后的內存,是否銷毀還是重新分配
PHP內存管理器
接口層,是一些宏定義。
**堆層 heap **
_zend_mm_heap
初始化內存,調用 zend_mm_startup
PHP內存管理維護三個列表:
1)小塊內存列表 free_buckets
2)大塊內存列表 large_free_buckets
3)剩余內存列表 rest_buckets
兩個HashTable 結構,難點是查找和計算內存地址
1)free_buckets
Hash函數為:
#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))
2)large_free_buckets
Hash函數為:
#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S)
static inline unsigned int zend_mm_high_bit(size_t _size){
..//省略若干不同環境的實現
unsignedint n =0;
while(_size !=0) {
_size = _size >>1; n++;}
return n-1;
}
存儲層 storage
- 內存分配的方式對堆層透明化,實現存儲層和heap層的分離。
- 不同的內存分配方案, 有對應的處理函數。
內存的申請
PHP底層對內存的管理, 圍繞著小塊內存列表(free_buckets)、 大塊內存列表(large_free_buckets)和 剩余內存列表(rest_buckets)三個列表來分層進行的
ZendMM向系統進行的內存申請,并不是有需要時向系統即時申請, 而是由ZendMM的最底層(heap層)先向系統申請一大塊的內存,通過對上面三種列表的填充, 建立一個類似于內存池的管理機制。 在程序運行需要使用內存的時候,ZendMM會在內存池中分配相應的內存供使用。 這樣做的好處是避免了PHP向系統頻繁的內存申請操作
ZendMM對內存分配的處理步驟:
1)內存檢查;
2)命中緩存,找到內存塊,調至步驟5;
3)在ZendMM管理的heap層存儲中搜索合適大小的內存塊, 是在三種列表中小到大進行的,找到block后,調至步驟5;
4)步驟3未找到內存,則使用 ZEND_MM_STORAGE_ALLOC 申請新內存塊 (至少為ZEND_MM_SEG_SIZE),進行步驟6
5)使用zend_mm_remove_from_free_list函數將已經使用block節點在zend_mm_free_block中移除;
6) 內存分配完畢,對zend_mm_heap結構中的各種標識型變量進行維護,包括large_free_buckets, peak,size等;
7) 返回分配的內存地址;
內存的銷毀
ZendMM在內存銷毀的處理上采用與內存申請相同的策略,當程序unset一個變量或者是其他的釋放行為時, ZendMM并不會直接立刻將內存交回給系統,而是只在自身維護的內存池中將其重新標識為可用, 按照內存的大小整理到上面所說的三種列表(small,large,free)之中,以備下次內存申請時使用。
ZendMM將內存塊以整理收回到zend_mm_heap的方式,回收到內存池中。
程序使用的所有內存,將在進程結束時統一交還給系統。
垃圾回收
自動回收內存的過程叫垃圾收集。PHP提供了語言層的垃圾回收機制,讓程序員不必過分關心程序內存分配。
PHP5.3之前
引用計數方式的內存動態管理。
PHP中所有的變量都是以zval變量的形式存在。
變量引用計數變為0時,PHP將在內存中銷毀這個變量。只是這里的垃圾并不能稱之為垃圾。并且PHP在一個生命周期結束后就會釋放此進程/線程所占的內容,這種方式決定了PHP在前期不需要過多考慮內存的泄露問題。
PHP5.3的垃圾回收
引入垃圾收集機制的目的是為了打破引用計數中的循環引用,從而防止因為這個而產生的內存泄露。 垃圾收集機制基于PHP的動態內存管理而存在。PHP5.3為引入垃圾收集機制,在變量存儲的基本結構上有一些變動.
struct _zval_struct {
/* Variable information */
zvalue_value value;/* value */
zend_uint refcount__gc;
zend_uchar type;/* active type */
zend_uchar is_ref__gc;
};
添加了 __gc 以用于新的垃圾回收機制。
PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems
PHP5.3的垃圾回收算法仍然以引用計數為基礎,但是不再是使用簡單計數作為回收準則,而是使用了一種同步回收算法,這個算法由IBM的工程師在論文Concurrent Cycle Collection in Reference Counted Systems中提出。
論文較復雜, 列出一些大體描述。
首先PHP會分配一個固定大小的“根緩沖區”,這個緩沖區用于存放固定數量的zval,這個數量默認是10,000,如果需要修改則需要修改源代碼Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新編譯。
由上文我們可以知道,一個zval如果有引用,要么被全局符號表中的符號引用,要么被其它表示復雜類型的zval中的符號引用。因此在zval中存在一些可能根(root)。這里我們暫且不討論PHP是如何發現這些可能根的,這是個很復雜的問題,總之PHP有辦法發現這些可能根zval并將它們投入根緩沖區。
當根緩沖區滿額時,PHP就會執行垃圾回收,此回收算法如下:
1、對每個根緩沖區中的根zval按照深度優先遍歷算法遍歷所有能遍歷到的zval,并將每個zval的refcount減1,同時為了避免對同一zval多次減1(因為可能不同的根能遍歷到同一個zval),每次對某個zval減1后就對其標記為“已減”。
2、再次對每個緩沖區中的根zval深度優先遍歷,如果某個zval的refcount不為0,則對其加1,否則保持其為0。
3、清空根緩沖區中的所有根(注意是把這些zval從緩沖區中清除而不是銷毀它們),然后銷毀所有refcount為0的zval,并收回其內存。
如果不能完全理解也沒有關系,只需記住PHP5.3的垃圾回收算法有以下幾點特性:
1、并不是每次refcount減少時都進入回收周期,只有根緩沖區滿額后在開始垃圾回收。
2、可以解決循環引用問題。
3、可以總將內存泄露保持在一個閾值以下。
PHP5.2與PHP5.3垃圾回收算法的性能比較
PHP Manual中的相關章節:http://docs.php.net/manual/zh/features.gc.performance-considerations.php
首先是內存泄露試驗,下面直接引用PHP Manual中的實驗代碼和試驗結果圖:
<?php
class Foo
{
public $var = '3.1415962654';
}
$baseMemory = memory_get_usage();
for ( $i = 0; $i <= 100000; $i++ )
{
$a = new Foo;
$a->self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
?>
可以看到在可能引發累積性內存泄露的場景下,PHP5.2發生持續累積性內存泄露,而PHP5.3則總能將內存泄露控制在一個閾值以下(與根緩沖區大小有關)。
與垃圾回收算法相關的PHP配置
1、可以通過修改php.ini中的zend.enable_gc來打開或關閉PHP的垃圾回收機制,也可以通過調用gc_enable()或gc_disable()打開或關閉PHP的垃圾回收機制。
2、在PHP5.3中即使關閉了垃圾回收機制,PHP仍然會記錄可能根到根緩沖區,只是當根緩沖區滿額時,PHP不會自動運行垃圾回收
3、當然,任何時候您都可以通過手工調用gc_collect_cycles()函數強制執行內存回收。