如何避免 PHP 5.4 中的内存泄漏?

How to avoid memory leaks in PHP 5.4?

我刚刚发现在 PHP 中可能会泄漏内存。我在循环中 运行 一些代码,每次循环后内存使用量都会增加,直到脚本达到内存限制。我已经确定:

这是一个示例脚本,演示了与 PhpExcel library 有关的问题:

require_once(__DIR__ . '/libraries/PHPExcel/PHPExcel.php');
ini_set('memory_limit', '200M');
@mkdir(__DIR__ . '/output');
gc_enable();

for ($n = 0 ; $n < 10 ; $n++)
{
    do_it($n);
    gc_collect_cycles();
}

function do_it($n)
{
    echo 'Round '.$n.'...';

    $text = str_repeat('x', 50000);

    $phpexcel = new PHPExcel();
    $worksheet = $phpexcel->getActiveSheet();

    for ($r = 1 ; $r < 50 ; $r++)
        for ($c = ord('A') ; $c <= ord('S') ; $c++)
            $worksheet->setCellValueExplicit(chr($c) . $r, $text, PHPExcel_Cell_DataType::TYPE_STRING);

    // $phpexcel->disconnectWorksheets();

    unset($phpexcel, $worksheet);

    echo 'done, now using ' . round((memory_get_usage()) / 1024 / 1024).' MB' . "\n";
}

输出:

Round 0...done, now using 41 MB
Round 1...done, now using 80 MB
Round 2...done, now using 123 MB
Round 3...done, now using 157 MB
Round 4...
Fatal error: Allowed memory size of 209715200 bytes exhausted (tried to allocate 36 bytes) 

现在针对这个特定问题 the solution is 在每个循环后调用 $phpexcel->disconnectWorksheets();,这会取消设置一些对象成员。

真正的问题是:作为一名 PHP 程序员,我应该怎么做才能避免此类内存泄漏?在取消设置对象之前,我真的必须递归遍历每个对象以取消设置其成员吗?

disconnectWorksheets() 方法是在 PHP 5.3 新奇的垃圾收集之前添加的。

问题是 PHPExcel 有循环引用。 PHPExcel 对象引用其工作表对象,而各个工作表引用其父 PHPExcel 对象。同样,工作表对象引用其所有单元格(通过缓存的单元格集合),并且所有单元格都引用其父工作表。

这种类型的循环关系无法用旧的 PHP 垃圾收集器清除,它完全依赖于引用计数;这意味着如果在别处存在对对象的任何引用,则无法取消设置对象。

disconnectWorksheets() 提供了一种从下到上打破这些循环关系的简单方法:断开单元格与其父工作表的连接,以便仅存在非循环工作表 -> 单元格关系,并且 PHPExcel 对象及其工作表。

一旦循环关系被打破,一个简单的 unset() 应该可以工作。

但是,我从代码中看到您正在创建对工作表的单独引用:

$worksheet = $phpexcel->getActiveSheet();

所以这个引用不会被调用 disconnectWorksheets() 清除,对 PHPExcel 对象的引用也将保留在编写器中。

我怀疑它可能归结为

unset($phpexcel, $worksheet, $writer);

取消设置对象。

如果它首先尝试取消设置 $phpexcel,那么它可能不能,因为在 $worksheet 中仍然有对它的引用,在 $writer 中还有另一个引用....也许颠倒了实体的顺序你取消设置会有所不同

unset($writer, $worksheet, $phpexcel);

或者可能在调用 disconnectWorksheets() 之前取消设置 $writer$worksheet;之后才取消设置 $phpexcel。


理论上,PHP5.3 的新垃圾回收应该可以处理这些循环引用,但在实践中我不确定它到底有多有效...我根本没有测试过.从理论上讲,它应该消除所有需要使用 disconnectWorksheets() 方法。

对于还在使用PHP <= 5.3的用户来说,disconnectWorksheets()方法真的是保留了(PHP5.2.0仍然是最早支持的版本;信不信由你不,我仍然有人要求我在这个周末解决 5.1.16 下的问题 运行 PHPExcel)。然而,很可能还有其他循环引用,也许在样式关系中,没有被调用 disconnectWorksheets() 清除,所以我什至不能保证这一点很遗憾;但这是我能提供的最好建议。

这里的问题是静态数组 PHPExcel_Calculation::$_workbookSets 为每个工作簿获取对 PHPExcel_Calculation 对象的引用。每次 do_it() 运行时它都会增长。因此,由于对象永远不会真正超出范围,因此无法回收它们的内存及其属性等。

PHPExcel_Calculation::unsetInstance($phpexcel); 替换你的 unset(...); 并且内存泄漏消失了,因为这从该数组中删除了关联的对象(并且只 那个。 )

对于一般性问题:循环引用不是问题,垃圾收集器可以很好地处理它们 - 避免使用全局变量(静态变量只是花哨的全局变量),因为它们可以很好地隐藏并且膨胀失控。