Memory Leaks With Objects in PHP 5

By | September 27, 2007

Wow, this entry is getting a lot of traffic from DZone and other places. If you like this post, be sure to check out New Year’s Benchmarks for PHP framework comparisons, and the Solar Framework for PHP 5. Thanks for visiting!

One of the nice things about using a scripting language is that it automates garbage collection for you. You don’t have to worry about releasing memory when you’re done with your variables. As each variable passes out of scope, PHP frees that memory for you. If you want to, you can free the memory yourself using unset(), but usually you don’t have to.

But there is at least one circumstance in which PHP will not free memory for you when you call unset(). Cf. http://bugs.php.net/bug.php?id=33595.

Problem

If you have two objects in circular reference, such as in a parent-child relationship, calling unset() on the parent object will not free the memory used for the parent reference in the child object. (Nor will the memory be freed when the parent object is garbage-collected.)

Confusing? Here’s a script you can run to see it in action:

<?php
class Foo {
    function __construct()
    {
        $this->bar = new Bar($this);
    }
}

class Bar {
    function __construct($foo = null)
    {
        $this->foo = $foo;
    }
}

while (true) {
    $foo = new Foo();
    unset($foo);
    echo number_format(memory_get_usage()) . "\n";
}
?>

Run that and watch your memory usage climb and climb, until memory runs out.

...
33,551,616
33,551,976
33,552,336
33,552,696
PHP Fatal error:  Allowed memory size of 33554432 bytes exhausted
(tried to allocate 16 bytes) in memleak.php on line 17

For most PHP developers this behavior is not likely to be a problem. However, if you use a lot of objects in parent-child relationships over a long-running script, you can run out of memory pretty quickly, especially if those objects are relatively large. I discovered this myself while testing some ORM-related pieces for Solar and it took me a couple days to figure it out — hence this blog post. ;-)

Userland Solution

The bugs.php.net link above presents a solution, albeit inelegant and tedious. The “fix” is to call a destructor method before unsetting the object. The destructor method should clear out any internal parent object references, which will free the memory that would otherwise leak.

The “fixed” script looks like this:

<?php
class Foo {
    function __construct()
    {
        $this->bar = new Bar($this);
    }
    function __destruct()
    {
        unset($this->bar);
    }
}

class Bar {
    function __construct($foo = null)
    {
        $this->foo = $foo;
    }
}

while (true) {
    $foo = new Foo();
    $foo->__destruct();
    unset($foo);
    echo number_format(memory_get_usage()) . "\n";
}
?>

Note the new Foo::__destruct() method and the call to $foo->__destruct() before unsetting. Now the script will run forever, showing you the amount of memory it uses (which never gets any bigger, thank goodness).

PHP Internals Solution?

Why does the memory leak occur? I am not wise in the ways of PHP internals, but it has something to do with reference counts. The refcount for the child $foo reference inside $bar does not get decremented when the parent $foo is unset, so PHP thinks the $foo object is still needed and doesn’t release the memory for it … or something like that. I am sure to be displaying my ignorance here, but the general idea is the same: a refcount is not decremented, so some memory never gets released.

I get from the bugs.php.net link above that modifying the garbage-collection process to check for this kind of issue would be a performance-killer, and what little I know about refcounts makes me think this is true.

Instead of changing the garbage-collection process, would it make sense to have unset() do some extra processing on the variable to look for internal objects and unset them too? (Or perhaps to call __destruct() on objects being unset?) Maybe a PHP internals person can comment here or elsewhere on the practicality and/or wisdom of such changes.

UPDATE: Martin Fjordvald notes in the comments that a patch from David Wang for garbage collection does exist and is under consideration. (Did I say “patch”? More like “whole sheet of cloth” — it’s huge. See the CVS checkout instructions at the end of that email.) Problem is that it didn’t garner many votes for inclusion in 5.3. A nice compromise solution might be to have the unset() function call the __destruct() method of objects sent to it; that would seem intuitively appropriate here.

50 thoughts on “Memory Leaks With Objects in PHP 5

  1. Stan

    Paul,
    I thought I heard that one of the SoC students was working on garbage collection improvements for PHP, any idea if the work he/she has done addresses any of this? I’ve ran into this issue too, it can be a real pain unfortunately.

    Pax,
    - Stan

    Reply
  2. Lukas

    Yes, there is a patch for 5.2, but right now it looks like the patch will not make it into PHP until version 6.

    Reply
  3. Pingback: PHPDeveloper.org

  4. rodrigo moraes

    @ Alex@Net: it seems it won’t be resolved in PHP 5.3. See the links in Martin Fjordvald’s comment: “Implement David’s Circular Garbage collection patch” didn’t had many votes. Unfortunately, because this is a *major* issue for object oriented systems.

    Reply
  5. Pingback: developercast.com » Paul Jones’ Blog: Memory Leaks With Objects in PHP 5

  6. Pingback: Sicherheit in PHP - Seite 3 - PHP @ tutorials.de: Forum, Tutorial, Anleitung, Schulung & Hilfe

  7. Andrew Minerd

    RE: Having unset call __destruct

    Unfortunately, this would completely kill any references to an existing object, for instance:

    $a = new MyObject();
    $b = $a;
    unset($a);

    The object should (correctly) live on in $b; calling __destruct when $a is unset would be incorrect. Determining when the instance is no longer being used is the entire problem: the PHP garbage collector cannot resolve recursive references, thus it cannot collect the instance.

    Reply
  8. pmjones Post author

    Hi Andrew — I see your point. It does look like a modification to the garbage collector is the only way to go. Thank you for the clarification and example!

    Reply
  9. pmjones Post author

    Hi David — as with __get() and __set(), the magic __unset() method applies to pseudo-properties of the object, not the object itself. Wish it was that easy. :-(

    Reply
  10. Sin

    Andrew,

    That example you posted works with the php4 mindset. In 4, objects were treated like primatives, and were copied for assignments and function calls. However, in 5, as a developer you are expected to clone objects that are needed but not in your control. Calling __destruct would be proper behavior for a proper OO language, but it turns out that PHP is just too nice to you people. Perhaps they should move to a manual reference count a la Obj-C’s retain/release.

    Reply
  11. Andrew Minerd

    Sin,

    That’s not entirely correct either. You are expected to clone objects when you actually want a copy. You can, however, assign two variables to the same instance of the object without cloning — if you couldn’t (or “shouldn’t”) hold multiple references to a single object instance, you’d never run into circular references in the first place.

    Reply
  12. Pingback: Mind dump » www apžvalga #1

  13. Rasmus Schultz

    My solution:

    A base class for all my objects that implements a generic destructor, e.g.:

    class BASE {
    function __destruct() {
    foreach ($this as $index => $value) unset($this->$index);
    }
    }

    This was no problem to implement, as all of my persistent classes use a base class anyway, so I just extended that – if your application is based on a framework of some sort, this is most likely true for you as well.

    One important thing to keep in mind, is if your classes do implement their own __destruct() method, that the destructor of the base class is NOT automatically called. Again, for me, this was no problem, as the majority of our classes did not have custom destructors. If any classes do implement a destructor, you must call parent::__destruct() at the end of it.

    In batch scripts and daemons, after each iteration, it is still necessary to manually call __destruct() and then unset() on every used object. My solution for that is the following small global function:

    function destroy(&$var) {
    if (is_object($var)) $var->__destruct();
    unset($var);
    }

    Now, instead of __destruct() and then unset() on every object you’re done with, just call destroy($object) instead.

    This also ensures that you get an error message for any objects you try to dispose of, where the __destruct() method is not implemented (or inherited), giving you an easy way to find the places where you need to fix this.

    In my case, this approach was used on a batch email system with 100.000+ lines of code. Implementing this solution took approximately one hour, and our script batch-processes thousands of email generations now without leaking any memory at all.

    I hope this helps somebody else! :-)

    Reply
  14. Mike

    solved on php 5? hahahaha… I am having this problem right now with 5.2!

    Reply
  15. Adam Cox

    We are using 5.1.6 and experiencing huge memory leaks. We’re not yet able to pinpoint it to PHP, but we believe it is and we know that it is Apache causing these leaks. Thanks for your insight, I have e-mailed this useful information to my team and we will look for any objects that instantiate other objects without the object instantiating the other object also unsetting it. Hopefully this is our problem, but we are having a memory flood rather than a leak and this may not be it.

    Reply
  16. Pingback: Daily Digest for 2008-10-08 | Pedro Trindade

  17. Pingback: Is Dreamhost PS simply a way for Dreamhost to wash their hands of support? » A Division by Zer0

  18. Piotr Nowak

    If you use classes from complicated structure, you cannot solve this in this way (without expensive refactoring)..

    In my project, i tried other way – redirecting to the same script. Simply version below:

    public function indexAction()
    {
    if (!isset($_SESSION['counter'])) {
    $_SESSION['counter'] = 0;
    $_SESSION['limit'] = 4;
    $_SESSION['memory_start'] = array();
    $_SESSION['memory_stop'] = array();
    }
    $counter = $_SESSION['counter'];
    $memory_start = $_SESSION['memory_start'];
    $memory_stop = $_SESSION['memory_stop'];

    if ($_SESSION['counter'] < $_SESSION['limit']) {

    $memory_start[] = memory_get_usage();
    $sth = array();
    $rnd = rand(10, 10000);
    for($i = 0; $i++ < $rnd;) {
    $sth[] = $i;
    }
    $counter++;
    $memory_stop[] = memory_get_usage();

    $_SESSION['counter'] = $counter;
    $_SESSION['memory_start'] = $memory_start;
    $_SESSION['memory_stop'] = $memory_stop;
    header(“Location: /test”,true,302);
    die();
    }
    echo “”;
    print_r ($_SESSION['memory_start']);
    print_r ($_SESSION['memory_stop']);
    echo “”;
    unset($_SESSION['counter']);
    die();
    }

    Reply
  19. eboraks

    It is maybe a stupid question. Is implementing factory design pattern can be use to manage this problem?

    Reply
  20. Pingback: Garbage Collection - Seite 3 - php.de

  21. Pingback: Getting out more from PHP « progressive thinking

  22. Pingback: Guest Post: Fix for Memory Leaks in Magento « Alexander Ringsdorffs Blog

  23. Pingback: Gastbeitrag: Fix für Speicherlecks in Magento « Alexander Ringsdorffs Blog

  24. Chris

    ehm, no i don’t think it’s solved in php 5.3 ! i wish it was, but it’s exactly the same! scripts are still grabbing gigabytes of RAM because of this.

    Reply
  25. Pingback: PHP Fatal error: Allowed memory exhausted | VerySimple, Inc.

  26. Pingback: ??????? » [Web] ????

  27. Pingback: 10 bonnes pratiques optimiser code PHP5 | Alfwed's blog | Alfwed

  28. Fabian

    Working perfectly in PHP 5.3.2 now. Seems to be fixed. At least Garbage Collection start every now and then and cleans memory a little.

    Reply
  29. vikas

    this is exactly happening with me. i am using PEARs to extract large number of xml files in a loop. memory consumption is always increasing on each cycle :(

    Reply
  30. Pingback: PHP 4 and PHP 5 « ARP’s Web Blog

  31. Miah

    I’m using someone else’s class and the implementation requires new object to be created in a loop. Will this cause memory leak? The class doesn’t seem to have function to release or empty the object so moving the object creation outside of the loop will result adding more and more content into the object. Any thoughts?

    Reply
  32. vandad

    Hello, I have a question.
    in this code
    1.Does “__destruct” work automatically after perform “new name(“Dan”)”?/in wampserver it works automatically!!
    2.I saw it’s better to write $foo->__destruct(); so what? )-:
    myname =$data;
    }
    function __destruct{
    unset($this->myname);
    }
    }
    $v = new name(“Dan”);
    unset($v);
    ?>

    Reply
  33. vandad

    Hello, I have a question.
    in this code
    1.Does “__destruct” work automatically after perform “new name”?/in wampserver it works automatically!!
    2.I saw it’s better to write $foo->__destruct(); so what? )-:
    myname =$data;
    }
    function __destruct{
    unset($this->myname);
    }
    }
    $v = new name(“Dan”);
    unset($v);
    ?>

    Reply
  34. Christian

    Thanks for this interesting article.

    It seems that this problem no longer exists with PHP 5.3.x – at least the example mentioned above works fine with PHP 5.3.3 and GC turned on. Maybe you should mention this in your post.

    Bye
    Christian

    Reply
  35. Pingback: Magento memory leaks and custom options » mandagreen.com

  36. Pingback: Finding memory leaks in PHP objects « coudenysj

  37. Pingback: What's better at freeing memory with PHP: unset() or $var = null - PHP Questions - Developers Q & A

  38. Pingback: Decent memory profiling tool for php | Technology & Programming

Leave a Reply

Your email address will not be published. Required fields are marked *