PHP的垃圾回收機制(建議收藏)

語言: CN / TW / HK

一、原理

php5和php7的垃圾回收機制都是利用引用計數

二、php5和php7不同點

1、PHP5標量資料型別會計數,PHP7標量資料型別不再計數,不需要單獨分配記憶體 2、PHP7的zval 需要的記憶體不再是單獨從堆上分配,不再自己儲存引用計數。 3、PHP7的複雜資料型別(比如陣列和物件)的引用計數由其自身來儲存。

三、變數在zval的變數容器中結構

zval中,除了儲存變數的型別和值之外,還有is_ref欄位和refcount欄位
    1、is_ref:是個bool值,用來區分變數是否屬於引用集合。
    2、refcount:計數器,表示指向這個zval變數容器的變數個數。

四、PHP5.3標量在zval容器例子

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

<?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

五、PHP7.X 標量在zval容器例子

<?php

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

結果如下:可以看到標量(布林,字串,整形,浮點型)不再計數了

六、PHP5.3複合型別陣列和物件在zval容器例子

<?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');

結果如下:可以看出,陣列用了比陣列長度多1個zval儲存。陣列分配了三個zval容器:a   meaning  number
a:(refcount=1, is_ref=0),
array
  'meaning' => (refcount=1, is_ref=0),
string
'life' (length=4)
  'number' => (refcount=1, is_ref=0),
int
test:(refcount=1, is_ref=0),
object(Test)[1]
  public 'a' => (refcount=2, is_ref=0),
int
  public 'b' => (refcount=2, is_ref=0),
int

七、PHP7.X複合型別陣列和物件在zval容器例子

<?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=2,後經測試發現數組的refcount都是從2開始的

八、迴圈引用問題

  1、PHP7.1效果

<?php

$a = array('life');
xdebug_debug_zval( 'a' );
echo PHP_EOL;
$a[] = &$a;
xdebug_debug_zval('a');

可以看到,箭頭方向表示的就是遞迴迴圈引用了

說明:在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,
   如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。現在unset ($a),那麼array的refcount減1變為1.現在無任何變數指向這個zval,
   而且這個zval的計數器為1,不會回收。

結果:儘管不再有某個作用域中的任何符號指向這個結構(就是變數容器),由於子元素“1”仍然指向陣列本身,所以這個容器不能被清除 。
   因為沒有另外的符號指向它,使用者沒有辦法清除這個結構,結果就會導致記憶體洩漏。

在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()開啟和關閉。

九、垃圾回收演算法

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、可以總將記憶體洩露保持在一個閾值以下。

以上內容希望幫助到大家,更多PHP大廠PDF面試文件,PHP進階架構視訊資料,PHP精彩好文免費獲取可以關注公眾號:PHP開源社群,或者訪問:

2021金三銀四大廠面試真題集錦,必看!

騰訊一面的Redis秒殺面試題你會麼?

四年精華PHP技術文章整理合集——PHP框架篇

四年精華PHP技術文合集——微服務架構篇

四年精華PHP技術文合集——分散式架構篇

四年精華PHP技術文合集——高併發場景篇

四年精華PHP技術文章整理合集——資料庫篇