It's at least partly to do with the fact that this is occurring at the end of the script. Normal reference counting rules are held in abeyance; partly for efficiency reasons and partly because otherwise the cleanup process could get stuck on things like circular references. When the script terminates, the internal object list is just cranked through from beginning to end, calling all of the destructors found; the objects are retained though, in case there are other objects whose destructors are still to be called that are referring to them. This is quicker and more effective than chasing references, and any transient problems caused by objects still sitting around after they're destroyed goes unnoticed, because the program state is just as consistent afterwards as it was beforehand - only now all the objects are gone. But that's okay, because everything else is gone as well.
To explicitly remove a (reference to) an object, $object = null; or unset($object); . Consider
$utility = new UtilityClass ( );
$task = new TaskClass ( $utility );
$task->doWork ( );
$task = null;
echo '$utility is still around.';
and
$utility = new UtilityClass ( );
$task = new TaskClass ( $utility );
$utility = null;
$task->doWork ( );
$task = null;
echo 'All gone!';
In short, word from Andi Guttmans is
A VERY important implication is that you cannot, and must not rely in any way on the order of destruction during shutdown. It runs in no particular order. That means that by the time the destructor for your object $foo is called, a reference to some other object $bar may already be post-destruction. Note that it will still be ‘intact’ and accessible in the sense that if you access it - there won’t be crashes. However, if this object does indeed have a destructor, it can be considered semantically wrong to touch this object at this point.