博客 / 詳情

返回

php內存泄露的原因分析和垃圾回收機制的探討

最近在寫某個腳本時,在循環內重複調用了某個方法。按照以前的理解,方法在執行完成後,局部變量就失效了,它申請內存就釋放了,但實際上並非如此。

<?php
class Foo
{
    public $var = '3.1415962654';
}

$baseMemory = memory_get_usage();

for ( $i = 0; $i <= 100000; $i++ )
{
    f($i, $baseMemory);
}

function f($i, $baseMemory)
{
    $a = new Foo;
    $a->self = $a;
    if ( $i % 500 === 0 )
    {
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
    }
}

運行上面這段代碼後發現,php的內存並不是離開函數就釋放,而是達到一定值後才會進行釋放(只討論php5.3之後的機制)。官方的説法是

首先,實現垃圾回收機制的整個原因是為了,一旦先決條件滿足,通過清理循環引用的變量來節省內存佔用。在PHP執行中,一旦根緩衝區滿了或者調用gc_collect_cycles() 函數時,就會執行垃圾回收。

也就是説只要根緩衝區滿了,php就會執行垃圾回收,釋放那些沒用到的內存。

那麼什麼是“根緩衝區”呢?根緩衝區就是拿來存放所有可能根(可以理解為php裏的變量)的容器,他的值是10000,可以修改PHP源碼文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然後重新編譯PHP,來修改這個10000值。

如果沒有修改過根緩衝區的值,觀察上面的代碼就會發現,每10000次,就會執行一次垃圾回收,也就是根緩衝區在第一萬次的時候被填滿了。

那麼問題就來了,如果我的單個變量佔內存比較大,那麼根緩衝區還沒填滿,就有可能把內存用完了,也就來不及重新分配內存,這就是可能導致內存泄漏的原因之一。比如下面這個例子

<?php
ini_set('memory_limit', '128M');

class Use10MClass
{
    public $var = null;

    public function __construct()
    {
        $this->var = str_pad('1', 10 * 1024 * 1024);

    }
}

$baseMemory = memory_get_usage();
echo "當前內存:", memory_get_usage(), "\n";

for ($i = 0; $i <= 100; $i++) {
    test($i, $baseMemory);
}

function test($i, $baseMemory)
{
    $b = new Use10MClass();
    $b->self = $b;
    echo sprintf('%8d: ', $i), memory_get_usage() - $baseMemory, "\n";

}

在第十一次循環時,就報了PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to all
ocate 10485785 bytes) 這個錯誤,由於單個變量所消耗的內存過多,根緩衝區才被填了11個,還沒來得及執行垃圾回收內存就被撐爆了。解決方法有兩種

  1. 財大氣粗的,直接加大分配內存🐶。但是這種方法一般用於應急使用,因為出現內存泄露,基本代表程序多多少少有些問題,最好是找到內存使用過多的原因。
  2. 在適當的時候調用gc_collect_cycles()主動進行垃圾回收,釋放多餘的空間。

所以上面的代碼最好的解決方法就是,隔一段時間就進行一次手動的垃圾回收。這樣程序就能順利跑完了。

<?php
ini_set('memory_limit', '128M');

class Use10MClass
{
    public $var = null;

    public function __construct()
    {
        $this->var = str_pad('1', 10 * 1024 * 1024);

    }
}

$baseMemory = memory_get_usage();
echo "當前內存:", memory_get_usage(), "\n";

for ($i = 0; $i <= 100; $i++) {
    test($i, $baseMemory);
    // 每八次進行一下垃圾回收
    if ($i % 8 === 0) {
        gc_collect_cycles();
    }
}

function test($i, $baseMemory)
{
    $b = new Use10MClass();
    $b->self = $b;
    echo sprintf('%8d: ', $i), memory_get_usage() - $baseMemory, "\n";

}

另外,unset變量並不會立即釋放內存,該溢出的時候還是會溢出的。在函數最後一句unset局部變量是沒有意義的。

總的來説就是變量雖然無法使用了,但是他所佔用的內存空間並沒有被佔用。一般的程序等待根緩衝區滿了,自動垃圾回收就可以了。但是對一些變量比較大的情況,可以在適當的時候執行gc_collect_cycles()主動進行垃圾回收,避免內存泄露。程序進行垃圾回收是會消耗一定的時間的,所以也不推薦頻繁調用gc_collect_cycles()。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.