1. 程式人生 > >PHP的垃圾回收機制詳解

PHP的垃圾回收機制詳解

最近由於使用php編寫了一個指令碼,模擬實現了一個守護程序,因此需要深入理解php中的垃圾回收機制。本文參考了PHP手冊。

在理解PHP垃圾回收機制(GC)之前,先了解一下變數的儲存。

php中變數存在於一個zval的變數容器中。結構如下:

 

 

型別

is_ref

refcount

 

 

zval中,除了儲存變數的型別和值之外,還有is_ref欄位和refcount欄位。

  • is_ref:是個bool值,用來區分變數是否屬於引用集合。什麼意思呢,你可以這麼認為:表示變數是否有一個以上的別名。 
  • refcount:計數器,表示指向這個zval變數容器的變數個數。 

兩者之間有這麼一個預設關係:當refcount值為1時,is_ref的值為false。因為refcount為1,此變數不可能有多個別名,也就不存在引用了。

安裝xdebug拓展之後,可以利用xdebug_debug_zval打印出zval容器詳情。

這裡有一點需要注意,將一個變數 = 賦值給另一個變數時,不會立即為新變數分配記憶體空間,而是在原變數的zval中給refcount加1。 只有當原變數或者發生改變時,才會為新變數分配記憶體空間,同時原變數的refcount減 1 。當然,如果unset原變數,新變數直接就使用原變數的zval而不是重新分配。

 &引用賦值時,原變數的is_ref 變為1,refcount 加1.  如果給一個變數&賦值,之前 = 賦值的變數會分配空間。

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $a  = 1; xdebug_debug_zval( 'a' ); echo  PHP_EOL; $b  $a ; xdebug_debug_zval( 'a' ); echo  PHP_EOL;   $c  = & $a ; xdebug_debug_zval( 'a' ); echo  PHP_EOL;   xdebug_debug_zval( 'b' ); echo  PHP_EOL; ?>

  執行結果如下:

a:(refcount=1, is_ref=0),int 1

a:(refcount=2, is_ref=0),int 1

a:(refcount=2, is_ref=1),int 1

b:(refcount=1, is_ref=0),int 1

 

 

上面描述的zval儲存的是標量,那複合型別的陣列是如何儲存的呢?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $a  array 'meaning'  =>  'life' 'number'  => 42 ); xdebug_debug_zval(  'a'  ); echo  PHP_EOL; class  Test{      public  $a  = 1;      public  $b  = 2;            function  handle(){          echo  'hehe' ;      } }   $test  new  Test(); xdebug_debug_zval( 'test' ); ?>

  執行結果如下:

a:(refcount=1, is_ref=0),

array
  'meaning' => (refcount=1, is_ref=0),

string

'life' (length=4)
  'number' => (refcount=1, is_ref=0),

int

 42

test:(refcount=1, is_ref=0),

object(Test)[1]
  public 'a' => (refcount=2, is_ref=0),

int

 1
  public 'b' => (refcount=2, is_ref=0),

int

2
 

可以看出,陣列用了比陣列長度多1個zval儲存。物件類似。下面給出了陣列的儲存形象表示

可以看到:陣列分配了三個zval容器:a   meaning  number

現在看看所謂的環狀引用是如何生成的

1 2 3 4 5 <?php $a  array 'one'  ); $a [] =&  $a ; xdebug_debug_zval(  'a'  ); ?>

  執行結果:

a:(refcount=2, is_ref=1),

array
  0 => (refcount=1, is_ref=0),

string

 'one' (length=3)
  1 => (refcount=2, is_ref=1), &array

a 和 1 的zval容器 是一樣的。如下:

 

 

這樣就形成了環狀引用。

在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。

現在unset ($a),那麼array的refcount減1變為1.現在無任何變數指向這個zval,而且這個zval的計數器為1,不會回收。

 

儘管不再有某個作用域中的任何符號指向這個結構(就是變數容器),由於陣列元素“1”仍然指向陣列本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,使用者沒有辦法清除這個結構,結果就會導致記憶體洩漏。慶幸的是,php將在請求結束時清除這個資料結構,但是在php清除之前,將耗費不少空間的記憶體。如果你要實現分析演算法,或者要做其他像一個子元素指向它的父元素這樣的事情,這種情況就會經常發生。當然,同樣的情況也會發生在物件上,實際上物件更有可能出現這種情況,因為物件總是隱式的被引用。

 

如果上面的情況發生僅僅一兩次倒沒什麼,但是如果出現幾千次,甚至幾十萬次的記憶體洩漏,這顯然是個大問題。在長時間執行的指令碼,比如請求基本上不會結束的守護程序時,就會出現問題,記憶體空間會不斷耗費,導致記憶體不足而崩潰。

 

PHP5.3中,採用了專門的演算法(比較複雜)。,來處理環狀引用導致記憶體洩露的問題。

當一個zval可能為垃圾時,回收演算法會把這個zval放入一個記憶體緩衝區。當緩衝區達到最大臨界值時(最大值可以設定),回收演算法會迴圈遍歷所有緩衝區中的zval,判斷其是否為垃圾,並進行釋放處理。或者我們在指令碼中使用gc_collect_cycles,強制回收緩衝區中的垃圾。

在php5.3的GC中,針對的垃圾做了如下說明:

1:如果一個zval的refcount增加,那麼此zval還在使用,肯定不是垃圾,不會進入緩衝區

2:如果一個zval的refcount減少到0, 那麼zval會被立即釋放掉,不屬於GC要處理的垃圾物件,不會進入緩衝區。

 3:如果一個zval的refcount減少之後大於0,那麼此zval還不能被釋放,此zval可能成為一個垃圾,將其放入緩衝區。PHP5.3中的GC針對的就是這種zval進行的處理。

 

 

開啟/關閉垃圾回收機制可以通過修改php配置實現,也可以在程式中使用gc_enable() 和 gc_disable()開啟和關閉。

 

開啟垃圾回收機制後,針對記憶體洩露的情況,可以節省大量的記憶體空間,但是由於垃圾回收演算法執行耗費時間,開啟垃圾回收演算法會增加指令碼的執行時間。

下面是php手冊中給的一個指令碼

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?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和5.3中記憶體的佔用情況,如下圖:

針對下面這個指令碼

1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class  Foo {      public  $var  '3.1415962654' ; }   for  $i  = 0;  $i  <= 1000000;  $i ++ ) {      $a  new  Foo;      $a ->self =  $a ; }   echo  memory_get_peak_usage(),  "\n" ; ?>

  

開啟垃圾回收機制,相對於不開啟的時候,指令碼執行時間增加了7%

通常,PHP中的垃圾回收機制,僅僅在迴圈回收演算法確實執行時會有時間消耗上的增加。但是在平常的(更小的)指令碼中應根本就沒有效能影響。