2012-12-31 01:44 阅读 19


What is the exact order of object deconstruction?

From testing, I have an idea: FIFO for the current scope.

class test1
    public function __destruct()
        echo "test1

class test2
    public function __destruct()
        echo "test2

$a = new test1();
$b = new test2();

Which produces the same results time and time again:


The PHP manual is vague (emphasis mine to highlight uncertainty): "The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence."

What is the exact order of deconstruction? Can anyone describe in detail the implementation of destruction order that PHP uses? And, if this order is not consistent between any and all PHP versions, can anyone pinpoint which PHP versions this order changes in?

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

2条回答 默认 最新

  • 已采纳
    dpauxqt1281 dpauxqt1281 2012-12-31 12:43

    First of all, a bit on general object destruction order is covered here:

    In this answer I will only concern myself with what happens when objects are still alive during the request shutdown, i.e. if they were not previously destroyed through the refcounting mechanism or the circular garbage collector.

    The PHP request shutdown is handled in the php_request_shutdown function. The first step during the shutdown is calling the registered shutdown functions and subsequently freeing them. This can obviously also result in objects being destructed if one of the shutdown functions was holding the last reference to some object (or if the shutdown function itself was an object, e.g. a closure).

    After the shutdown functions have run the next step is the one interesting to you: PHP will run zend_call_destructors, which then invokes shutdown_destructors. This function will (try to) call all destructors in three steps:

    1. First PHP will try to destroy the objects in the global symbol table. The way in which this happens is rather interesting, so I reproduced the code below:

      int symbols;
      do {
          symbols = zend_hash_num_elements(&EG(symbol_table));
          zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
      } while (symbols != zend_hash_num_elements(&EG(symbol_table)));

      The zend_hash_reverse_apply function will walk the symbol table backwards, i.e. start with the variable that was created last and going towards the variable that was created first. While walking it will destroy all objects with refcount 1. This iteration is performed until no further objects are destroyed with it.

      So what this basically does is a) remove all unused objects in the global symbol table b) if there are new unused objects, remove them too c) and so on. This way of destruction is used so objects can depend on other objects in the destructor. This usually works fine, unless the objects in the global scope have complicated (e.g. circular) interrelations.

      The destruction of the global symbol table differs significantly from the destruction of all other symbol tables. Normally symbol tables are destructed by walking them forward and just dropping the refcount on all objects. For the global symbol table on the other hand PHP uses a smarter algorithm that tries to respect object dependencies.

    2. The second step is calling all remaining destructors:

      zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);

      This will walk all objects (in order of creation) and call their destructor. Note that this only calls the "dtor" handler, but not the "free" handler. This distinction is internally important and basically means that PHP will only call __destruct, but will not actually destroy the object (or even change its refcount). So if other objects reference the dtored object, it will still be available (even though the destructor was already called). They will be using some kind of "half-destroyed" object, in a sense (see example below).

    3. In case the execution is stopped while calling the destructors (e.g. due to a die) the remaining destructors are not called. Instead PHP will mark the objects are already destructed:

      zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);

      The important lesson here is that in PHP a destructor is not necessarily called. The cases when this happens are rather rare, but it can happen. Furthermore this means that after this point no more destructors will be called, so the remainder of the (rather complicated) shutdown procedure does not matter anymore. At some point during the shutdown all the objects will be freed, but as the destructors have already been called this is not noticeable for userland.

    I should point out that this is the shutdown order as it currently is. This has changed in the past and may change in the future. It's not something you should rely on.

    Example for using an already destructed object

    Here is an example showing that it is sometimes possible to use an object that already had its destructor called:

    class A {
        public $state = 'not destructed';
        public function __destruct() { $this->state = 'destructed'; }
    class B {
        protected $a;
        public function __construct(A $a) { $this->a = $a; }
        public function __destruct() { var_dump($this->a->state); }
    $a = new A;
    $b = new B($a);
    // prevent early destruction by binding to an error handler (one of the last things that is freed)
    set_error_handler(function() use($b) {});

    The above script will output destructed.

    点赞 评论 复制链接分享
  • dongshan8953 dongshan8953 2012-12-31 08:01

    What is the exact order of deconstruction? Can anyone describe in detail the implementation of destruction order that PHP uses? And, if this order is not consistent between any and all PHP versions, can anyone pinpoint which PHP versions this order changes in?

    I can answer three of these for you, in a somewhat roundabout way.

    The exact order of destruction is not always clear, but is always consistent given a single script and PHP version. That is, the same script running with the same parameters that creates objects in the same order will basically always get the same destruction order as long as it runs on the same PHP version.

    The shutdown process -- the thing that triggers object destruction when script execution has stopped -- has changed in the recent past, at least twice in a way that impacted the destruction order indirectly. One of these two introduced bugs in some old code I had to maintain.

    The big one was back in 5.1. Prior to 5.1, the user's session was written to disk at the very start of the shutdown sequence, before object destruction. This meant that session handlers could access anything that was left over object-wise, like, say, custom database access objects. In 5.1, sessions were written after one sweep of object destruction. In order to retain the previous behavior, you had to manually register a shutdown function (which are run in order of definition at the start of shutdown before destruction) in order to successfully write session data if the write routines needed a (global) object.

    It is not clear if the 5.1 change was intended or was a bug. I've seen both claimed.

    The next change was in 5.3, with the introduction of the new garbage collection system. While the order of operations at shutdown remained the same, the precise order of destruction could now change based on ref counting and other delightful horrors.

    NikiC's answer has details on the current (at time of writing) internal implementation of the shutdown process.

    Once again, this is not guaranteed anywhere, and the documentation very expressly tells you to never assume a destruction order.

    点赞 评论 复制链接分享