Circular references and PHP's memory management

Bug #613851 reported by Velveeta
10
This bug affects 2 people
Affects Status Importance Assigned to Milestone
CoughPHP
Confirmed
Undecided
Richard Pistole
1.4
New
Undecided
Unassigned

Bug Description

When loading objects and collections that are related to each other, Cough automatically backlinks certain things to others to keep resource usage to a minimum, rather than duplicating the object in memory for all instances where they link up... Example:

foreach($collection as $element) {
        $element->setWhatever_Collection($this);
}

However, if you happen to be running a loop of things, and instantiating a collection into the same variable name over and over, i.e.:

foreach($some_Collection as $element) {
        $someOther_Collection = SomeOther_Collection::loadNonRetired(array(
                'foreign_key_id' => $element->getKeyId()
        ));

        // do some other stuff with $someOther_Collection
}

Obviously, this is bad coding practice, but is done in some situations nonetheless... The problem is that PHP's memory management won't reclaim any of the memory used in this situation, when the $someOther_Collection variable is overwritten and eventually goes out of scope...

Garbage collection only reclaims memory when there are no outstanding references to it, so even if you explicitly unset($someOther_Collection), memory is still lost to the system, because within $someOther_Collection are objects that are backlinked to the collection itself... All you've done is orphaned the memory when you unset your reference to that variable... This happens for all CoughObject and CoughCollection class items when they've loaded any data that points back to themselves, and they're unset or go out of scope, PHP can't reclaim the memory used on them...

The solution is simple, add destructors to both the CoughObject and CoughCollection classes to recursively unset all references in their objects and collections members, like so:

abstract class CoughObject {
        public function __destruct() {
                foreach($this->objects as $object) {
                        unset($object);
                }

                foreach($this->collections as $collection) {
                        unset($collection);
                }
        }
}

abstract class CoughCollection extends ArrayObject {
        public function __destruct() {
                foreach($this as $object) {
                        unset($object);
                }
        }
}

The destructors will fire off anytime a variable goes out of scope, is overwritten, or is explicitly unset(), and will recursively destroy their way down to the bottom of any other nested objects or collections, allowing that memory to be reclaimed by the system... This is PHP bug #33595 (http://bugs.php.net/33595), and has been deemed "not very critical, because all memory is freed on request shutdown" as of 5 years ago, so I don't see them implementing their own fix anytime soon :D

Revision history for this message
Richard Pistole (pistole) wrote :

I was talking with Velveeta on IRC about this, I'm going to get this into my branch when I have the time. Seems like a relatively quick/decent fix to a problem with the way PHP's GC is broken with orphaned object graphs.

Changed in coughphp:
assignee: nobody → Richard Pistole (pistole)
status: New → Confirmed
Revision history for this message
Richard Pistole (pistole) wrote :

Having some difficulty reproducing in a way that the fix works for.

Revision history for this message
Richard Pistole (pistole) wrote :

My test case: http://pastie.org/1077923 though you'll need to add a static $count = 0; To CoughObject and self:$count++ and self::$count-- to the constructor and destructor and an accessor....

Revision history for this message
Richard Pistole (pistole) wrote :

$author's destructor never gets called because of the circular references.

Revision history for this message
Velveeta (rllindsey) wrote :

i'm not sure how the addBook() function works in your pastie example, does it do a call to $author->getBook_Collection()->add($book), and in addition, do a $book->setAuthor_Object($author)... ? because if there's no circular reference being created, then you won't see an effect from this fix... if it's only adding $book into the author's book collection, without linking back to author, then when author is destroyed, the book references should be destroyed as well (as long as there are no other variables holding onto references to those books)... but if the books are pointing properly at the author object, then when that object is destroyed, it won't even attempt to recycle it because it'll see references still pointing at that memory slot from within the object itself... if you could either pastie your addBook() function, or confirm the behavior of it, it might help me figure out why you're not seeing results...

Revision history for this message
Richard Pistole (pistole) wrote :
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.