Search code examples
phpcphp-extension

Memory leak when creating cycled object in PHP7 extension


The following is the test function I created (for PHP 7.1).

PHP_FUNCTION(tsc_test3)
{
    zend_string *cnA;
    zend_class_entry *ceA;

    // $ret = new ClsA();
    cnA = zend_string_init("ClsA", 4, 0);
    ceA = zend_fetch_class(cnA, ZEND_FETCH_CLASS_DEFAULT);
    zend_string_release(cnA);
    object_init_ex(return_value, ceA);

    // $ret->propA = $ret;
    zval objA;
    ZVAL_COPY(&objA, return_value);
    zend_update_property(ceA, return_value, "propA", 5, &objA);
    zval_ptr_dtor(&objA);

    return;
}

As suggested in the comment, it returns a cycled object of ClsA. The following is the test PHP program for the function.

<?php
class ClsA {
    public $propA = 1;
}

$x = tsc_test3();

echo "DUMP1 ----\n";
var_dump($x);

for ($i = 0; $i < 10; $i++) {
    echo "Memory usage: ". memory_get_usage(). "\n";
    $x = tsc_test3();
}

echo "DUMP2 ----\n";
var_dump($x);

$x->propA = null;

echo "DUMP3 ----\n";
var_dump($x);

Here is the output of the PHP code.

DUMP1 ----
object(ClsA)#1 (1) {
  ["propA"]=>
  *RECURSION*
}
Memory usage: 351336
Memory usage: 351392
Memory usage: 351448
Memory usage: 351504
Memory usage: 351560
Memory usage: 351616
Memory usage: 351672
Memory usage: 351728
Memory usage: 351784
Memory usage: 351840
DUMP2 ----
object(ClsA)#11 (1) {
  ["propA"]=>
  *RECURSION*
}
DUMP3 ----
object(ClsA)#11 (1) {
  ["propA"]=>
  NULL
}

The var_dump() result looks fine, but memory usage constantly increases.

When I use ZVAL_COPY_VALUE instead of ZVAL_COPY, the memory usage doesn't increase, but it produces a weird output in DUMP3.

DUMP3 ----
*RECURSION*

May be the function returns a corrupted object.

Can anybody tell me what's wrong in the extension function?

Edit1: Just after posting the question I noticed memory_get_usage(true) doesn't increase. Is this the mistake I made?

Edit2: The following PHP program (pure PHP, no extension) shows increasing memory usage. Is this a PHP bug or am I misunderstanding something? I'm using PHP 7.1.28.

<?php
class ClsA {
    public $propA = 1;
}

for ($i = 0; $i < 10; $i++) {
    echo "Memory usage: ". memory_get_usage(). "\n";
    $x = new ClsA();
    $x->propA = $x;
}

Solution

  • This is due to the fact that the PHP garbage collector doesn't always reclaim the memory used by your object (a ClsA instance) at the exact moment you assign your reference ($x) to another object. PHP uses ref counting and garbage collection altogether.

    If you force a garbage collection on each loop, you will see that your memory footprint will remain the same :

    <?php
    class ClsA {
        public $propA = 1;
    }
    
    for ($i = 0; $i < 10; $i++) {
        gc_collect_cycles();
        echo "Memory usage: ". memory_get_usage(). "\n";
        $x = new ClsA();
        $x->propA = $x;
    }
    

    Output :

    $ php test.php
    Memory usage: 396296 <-- before the first ClsA allocation
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    Memory usage: 396384
    

    More (technical) info here : https://www.php.net/manual/en/features.gc.collecting-cycles.php