1

最近在写某个脚本时,在循环内重复调用了某个方法。按照以前的理解,方法在执行完成后,局部变量就失效了,它申请内存就释放了,但实际上并非如此。

<?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()。


wangshantao
86 声望3 粉丝

undefined