PHP的垃圾回收機制詳解

jopen 11年前發布 | 15K 次閱讀 PHP PHP開發

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

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

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

類型

</td> </tr>

</td> </tr>

is_ref

</td> </tr>

refcount

</td> </tr> </tbody> </table>

zval中,除了存儲變量的類型和值之外,還有is_ref字段和refcount字段。

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

    兩者之間有這么一個默認關系:當refcount值為1時,is_ref的值為false。因為refcount為1,此變量不可能有多個別名,也就不存在引用了。

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

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

     &引用賦值時,原變量的is_ref 變為1,refcount 加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

    上面描述的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=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存儲。對象類似。下面給出了數組的存儲形象表示

     PHP的垃圾回收機制詳解

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

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

    <?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容器 是一樣的。如下:

     PHP的垃圾回收機制詳解

    這樣就形成了環狀引用。

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

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

     PHP的垃圾回收機制詳解

    盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由于數組元素“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手冊中給的一個腳本

    <?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中內存的占用情況,如下圖:

     PHP的垃圾回收機制詳解

    針對下面這個腳本

    <?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中的垃圾回收機制,僅僅在循環回收算法確實運行時會有時間消耗上的增加。但是在平常的(更小的)腳本中應根本就沒有性能影響。

    來自:http://www.cnblogs.com/taijun/p/4206770.html

     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色