Memory leak?! Is Garbage Collector doing right whe

2019-05-07 08:02发布

问题:

I found following solution here on StackOverflow to get an array of a specific object property from array of objects: PHP - Extracting a property from an array of objects

The proposed solution is to use array_map and within create a function with create_function as following:

$catIds = array_map(create_function('$o', 'return $o->id;'), $objects);

What happens?: array_map runs through each array element in this case a stdClass object. First it creates a function like this:

function($o) {
    return $o->id;
}

Second it calls this function for the object in the current iteration. It works, it works nearly same like this similar solution:

$catIds = array_map(function($o) { return $o->id; }, $objects);

But this solution is only running in PHP version >= 5.3 because it uses the Closure concept => http://php.net/manual/de/class.closure.php

Now the real problem:

The first solution with create_function increases the memory, because the created function will be written to the memory and not be reused or destroyed. In the second solution with Closure it will.

So the solutions gives the same results but have different behaviors with respect to the memory.

Following example:

// following array is given
$objects = array (
    [0] => stdClass (
        [id] => 1
    ),
    [1] => stdClass (
        [id] => 2
    ),
    [2] => stdClass (
        [id] => 3
    )
)

BAD

while (true)
{
    $objects = array_map(create_function('$o', 'return $o->id;'), $objects);
    // result: array(1, 2, 3);

    echo memory_get_usage() ."\n";

    sleep(1);
}

4235616
4236600
4237560
4238520
...

GOOD

while (true)
{
    $objects = array_map(function($o) { return $o->id; }, $objects);
    // result: array(1, 2, 3);

    echo memory_get_usage() ."\n";

    sleep(1);
}

4235136
4235168
4235168
4235168
...

I spend so many time to find this out and now I want to know, if it's a bug with the garbage collector or do I made a mistake? And why it make sense to leave the already created and called function in memory, when it'll never be reuse?

Here is a running example: http://ideone.com/9a1D5g

Updated: When I recursively search my code and it's dependencies e.g. PEAR and Zend then I found this BAD way too often.

Updated: When two functions are nested, we proceed from the inside out in order to evaluate this expression. In other words, it is first starting create_function (once) and that returning function name is the argument for the single call of array_map. But because GC forget to remove it from memory (no pointer left to the function in memory) and PHP not be able to reuse the function already located in memory let me think that there is an error and not only a thing with "bad performance". This specific line of code is an example in PHPDoc and reused in so many big frameworks e.g. Zend and PEAR and more. With one line more you can work around this "bug", check. But I'm not searching for a solution: I'm searching for the truth. Is it a bug or is it just my approach. And latter I could not decide yet.

回答1:

In the case of create_function() a lambda-style function is created using eval(), and a string containing its name is returned. That name is then passed as argument to the array_map() function.

This differs from the closure-style anonymous function where no string containing a name is used at all. function($o) { return $o->id; } IS the function, or rather an instance of the Closure class.

The eval() function, inside create_function(), executes a piece of PHP code which creates the wanted function. Somewhat like this:

function create_function($arguments,$code) 
{
  $name = <_lambda_>; // just a unique string
  eval('function '.$name.'($arguments){$code}');
  return $name;
}

Note that this is a simplification.

So, once the function is created it will persist until the end of the script, just like normal functions in a script. In the above BAD example, a new function is created like this on every iteration of the loop, taking up more and more memory.

You can, however, intentionally destroy the lambda-style function. This is quite easy, just change the loop to:

while (true)
{
    $func = create_function('$o', 'return $o->id;');
    $objects = array_map($func, $objects);
    echo memory_get_usage() ."\n";
    sleep(1);
}

The string containting the reference (= name) to the function was made expliciet and accessible here. Now, every time create_function() is called, the old function is overwritten by a new one.

So, no, there's no 'Memory leak', it is meant to work this way.

Of course the code below is more efficient:

$func = create_function('$o', 'return $o->id;');

while (true)
{
    $objects = array_map($func, $objects);
    echo memory_get_usage() ."\n";
    sleep(1);
}

And should only be used when closure-style anonymous function are not supported by your PHP version.



回答2:

Don't use create_function() if you can avoid it. Particularly not repeatedly. Per the big yellow Caution box in the PHP manual:

...it has bad performance and memory usage characteristics.



回答3:

OK, I think the problem is, that first solution with create_function is running on older versions of PHP and the second solution doesn't increase the memory unnecessary. But let's have a look at first solution. The create_function method is called inside the array_map, namely for each while iteration. If we want a solution to work with older PHP versions and without increasing memory we have to do following to the older function instance on each while iteration:

$func = create_function('$o', 'return $o->id;');
$catIds = array_map($func, $objects);

That's all. So simple.

But it also isn't answering the question at all. What remains is the question if it is a bug with PHP or a feature. For my understanding that way to write the result of create_function in a variable SHOULD be the same as put it directly as parameter in array_map, isn't it?