1. 程式人生 > >PHP記憶體管理 垃圾回收

PHP記憶體管理 垃圾回收

概述
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記憶體管理器

clipboard.png

介面層,是一些巨集定義。
**堆層 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) 返回分配的記憶體地址;

PHP記憶體管理器

記憶體的銷燬

ZendMM在記憶體銷燬的處理上採用與記憶體申請相同的策略,當程式unset一個變數或者是其他的釋放行為時, ZendMM並不會直接立刻將記憶體交回給系統,而是隻在自身維護的記憶體池中將其重新標識為可用, 按照記憶體的大小整理到上面所說的三種列表(small,large,free)之中,以備下次記憶體申請時使用。

ZendMM將記憶體塊以整理收回到zend_mm_heap的方式,回收到記憶體池中。
程式使用的所有記憶體,將在程序結束時統一交還給系統。

垃圾回收

自動回收記憶體的過程叫垃圾收集。PHP提供了語言層的垃圾回收機制,讓程式設計師不必過分關心程式記憶體分配。

PHP變數儲存在一個zval容器裡面的
1.變數型別 2. 變數值 3. is_ref 代表是否有地址引用 4. refcount 指向該值的變數數量

變數賦值的時候:is_ref為false, refcount為1

$a = 1;
xdebug_debug_zval('a');
echo PHP_EOL;//換行符,提高程式碼的原始碼級可移植性

輸出:

a:

(refcount=1, is_ref=0),

int

 1

將變數a的值賦給變數b,變數b不會立刻去在記憶體中儲存值,而是先指向變數a的值,一直到變數a有任何操作的時候

$b = $a;
xdebug_debug_zval('a');
echo PHP_EOL;

輸出:

a:

(refcount=2, is_ref=0),

int

 1
$c = &$a;
xdebug_debug_zval('a');
echo PHP_EOL;

xdebug_debug_zval('b');
echo PHP_EOL;

輸出:

a:

(refcount=2, is_ref=1),

int

 1

b:

(refcount=1, is_ref=0),

int

 1

因為程式又操作了變數a,所以變數b會自己申請一塊記憶體將值放進去。
所以變數a的zval容器中refcount會減1變為1,變數c指向a,所以refcount會加1變為2,is_ref變為true

垃圾回收
1.在5.2版本或之前版本,PHP會根據refcount值來判斷是不是垃圾
如果refcount值為0,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、可以總將記憶體洩露保持在一個閾值以下。
4、如果發現一個zval容器中的refcount在增加,說明不是垃圾
5、如果發現一個zval容器中的refcount在減少,如果減到了0,直接當做垃圾回收
6、如果發現一個zval容器中的refcount在減少,並沒有減到0,PHP會把該值放到緩衝區,當做有可能是垃圾的懷疑物件
當緩衝區達到臨界值,PHP會自動呼叫一個方法取遍歷每一個值,如果發現是垃圾就清理

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";
   }
}

?>

gc-benchmark.png

可以看到在可能引發累積性記憶體洩露的場景下,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()函式強制執行記憶體回收。