AndreKR
AndreKR

Reputation: 33697

How to avoid memory leaks in PHP 5.4?

I just found out the hard way that it is possible to leak memory in PHP. I'm running some code in a loop and after each cycle the memory usage increases until the script hits the memory limit. I already made sure:

This is an example script that demonstrates the problem in regard to the 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";
}

Output:

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) 

Now for this particular problem the solution is to call $phpexcel->disconnectWorksheets(); after each cycle, which unsets some object members.

The real question is: What am I, as a PHP programmer, supposed to do to avoid such memory leaks? Do I really have to recursively traverse each object to unset its members before I can unset the object?

Upvotes: 4

Views: 1415

Answers (2)

user3942918
user3942918

Reputation: 26413

The problem here is that the static array PHPExcel_Calculation::$_workbookSets gets a reference to the PHPExcel_Calculation object for each workbook. Every time do_it() runs this grows. Since the objects are therefore never really out of scope, their memory and that of their properties and so on cannot be reclaimed.

Replace your unset(...); with PHPExcel_Calculation::unsetInstance($phpexcel); and the memory leak goes away, as this removes the associated object from that array (and does only that.)

To the general question: cyclic references aren't the issue, the garbage collector handles them just fine - avoid globals (statics are just fancy globals) as they can hide well and balloon out of control.

Upvotes: 2

Mark Baker
Mark Baker

Reputation: 212522

The disconnectWorksheets() method was added pre-dating PHP 5.3's fancy new garbage collection.

The problem is that PHPExcel has cyclic references. A PHPExcel object references its worksheet objects, and the individual worksheets reference their parent PHPExcel object. Likewise, a worksheet object references all of its cells (via the cached cell collection), and the cells all reference their parent worksheet.

This type of cyclic relationship couldn't be cleaned with the old PHP garbage collector, which worked purely off the reference count; meaning that objects couldn't be unset if any references to them existed elsewhere.

disconnectWorksheets() provided a simplistic method that breaks those cyclic relationships from bottom to top: disconnecting the cells from their worksheet parent so that only the non cyclic worksheet -> cell relationship exists, and similarly between the PHPExcel object and its worksheets.

Once the cyclic relationships are broken, a simple unset() should work.

However, I see from the code that you're creating a separate reference to the worksheet:

$worksheet = $phpexcel->getActiveSheet();

So this reference won't be cleaned by a call to disconnectWorksheets(), and a reference to the PHPExcel object will also be retained in the writer.

I suspect then that it might come down to the order in which

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

unsets the objects.

If it tries to unset $phpexcel first, then it probably can't because there's still a reference to it in $worksheet, and another in $writer.... perhaps reversing the order of the entities that you're unsetting will make that difference

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

or possibly unsetting $writer and $worksheet before calling disconnectWorksheets(); and only unsetting $phpexcel after that.


In theory, PHP 5.3's new garbage collection should handle these cyclic references, but in practise I'm not sure how effective it really is... it's not something I've tested at all. In theory, it should eliminate all need to use the disconnectWorksheets() method.

The disconnectWorksheets() method has really been retained for those users who still use PHP <= 5.3 (PHP 5.2.0 is still the earliest supported version; and believe it or not, I still had somebody asking me to address an issue running PHPExcel under 5.1.16 just this weekend). However, it's quite possible that there are other cyclic references, perhaps within the styles relations, that aren't being cleaned by a call to disconnectWorksheets(), so I can't even guarantee this unfortunately; but it's the best advice that I can offer.

Upvotes: 0

Related Questions