views:

344

answers:

4

I seem to recall a way to setup the __destruct for a class in such a way that it would ensure that circular references would be cleaned up as soon as the outside object falls out of scope. However, the simple test I built seems to indicate that this is not behaving as I had expected/hoped.

Is there a way to setup my classes in such a way that PHP would clean them up correctly when the outermost object falls out of scope?

I am not looking for alternate ways to write this code, I am looking for whether or not this can be done, and if so, how? I generally try to avoid these types of circular references where possible.

class Bar {
    private $foo;
    public function __construct($foo) {
        $this->foo = $foo;
    }
    public function __destruct() {
        print "[destroying bar]\n";
        unset($this->foo);
    }
}

class Foo {
    private $bar;
    public function __construct() {
        $this->bar = new Bar($this);
    }
    public function __destruct() {
        print "[destroying foo]\n";
        unset($this->bar);
    }
}

function testGarbageCollection() {
    $foo = new Foo();
}

for ( $i = 0; $i < 25; $i++ ) {
    echo memory_get_usage() . "\n";
    testGarbageCollection();
}

The output looks like this:

60440
61504
62036
62564
63092
63620
 [ destroying foo ]
 [ destroying bar ]
 [ destroying foo ]
 [ destroying bar ]
 [ destroying foo ]
 [ destroying bar ]
 [ destroying foo ]
 [ destroying bar ]
 [ destroying foo ]
 [ destroying bar ]

What I had hoped for:

60440
 [ destorying foo ]
 [ destorying bar ]
60440
 [ destorying foo ]
 [ destorying bar ]
60440
 [ destorying foo ]
 [ destorying bar ]
60440
 [ destorying foo ]
 [ destorying bar ]
60440
 [ destorying foo ]
 [ destorying bar ]
60440
 [ destorying foo ]
 [ destorying bar ]

UPDATE:

There are several great answers to this question relating to PHP > 5.3, but I selected the answer that would work with PHP < 5.3 since it actually relates to my project ( PHP 5.2.x ).

A: 

Since 5.3 you can

Col. Shrapnel
+2  A: 

A solution could be, with PHP >= 5.3, to use what's explained in the Garbage Collection section of the manual.

Especially, the gc_* functions could be of interest -- see gc_collect_cycles, amongst others.


In the case of the portion of code you posted, with PHP >= 5.3 :

  • the garbage collection should work
  • BUT it will only be run when PHP thinks it is necessary !

The second point is pretty important : as your code is short, it doesn't require much memory ; which means the garbage collection will not be run at the end of each iteration of the loop :

  • OK, this would free a little bit of memory
  • But that's not needed, as there is plenty of memory left anyway

And, as garbage collection takes time, PHP doesn't run it too often.


I wrote an article on my blog some time ago about that, where I did some tests ; it's in french, but the graph (no language barrier, here) in this section clearly shows that the garbage collector is run once in a while, when needed.

Pascal MARTIN
+1  A: 

http://docs.php.net/features.gc.collecting-cycles:

When the garbage collector is turned on, the cycle-finding algorithm as described above is executed whenever the root buffer runs full. The root buffer has a fixed size of 10,000 possible roots (although you can alter this by changing the GC_ROOT_BUFFER_MAX_ENTRIES constant in Zend/zend_gc.c in the PHP source code, and re-compiling PHP). When the garbage collector is turned off, the cycle-finding algorithm will never run. However, possible roots will always be recorded in the root buffer, no matter whether the garbage collection mechanism has been activated with this configuration setting.

http://docs.php.net/features.gc.performance-considerations:

First of all, the whole reason for implementing the garbage collection mechanism is to reduce memory usage by cleaning up circular-referenced variables as soon as the prerequisites are fulfilled. In PHP's implementation, this happens as soon as the root-buffer is full, or when the function gc_collect_cycles() is called.
VolkerK
+1  A: 

As __destruct is only called once the object is being reclaimed, you can't use it for that. You can create a manual cleanup function though:

class Foo {
  private $bar;
  public function __construct() {
    $this->bar = new Bar($this);
  }
  public function cleanup() {
    $this->bar = null;
  }
  public function __destruct() {
    print "[destroying foo]\n";
  }
}

class Bar {
  private $foo;
  public function __construct($foo) {
    $this->foo = $foo;
  }
  public function __destruct() {
    print "[destroying bar]\n";
  }
}

function testGarbageCollection() {
  $foo = new Foo();
  $foo->cleanup();
}

I'm not sure how useful this is, but that's really your only option < 5.3

troelskn