在PHP中破坏对象的顺序是什么?

时间:2012-12-31 01:44:11

标签: php php-internals

对象解构的确切顺序是什么?

从测试开始,我有一个想法:当前范围的FIFO。

class test1
{
    public function __destruct()
    {
        echo "test1\n";
    }
}

class test2
{
    public function __destruct()
    {
        echo "test2\n";
    }
}

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

一次又一次地产生相同的结果:

test1
test2

PHP manual含糊不清(强调我强调不确定性):“只要在关闭序列期间没有其他对特定对象,或任何顺序的引用,就会调用析构函数方法“。

解构的确切顺序是什么?任何人都可以详细描述PHP使用的销毁顺序的实现吗?而且,如果这个顺序在任何和所有PHP版本之间不一致,那么任何人都可以查明这个订单改变的PHP版本吗?

2 个答案:

答案 0 :(得分:34)

首先,这里介绍了一般对象销毁顺序:https://stackoverflow.com/a/8565887/385378

在这个回答中,我只关心在请求关闭期间对象仍然存活时发生的事情,即如果它们之前没有通过引用计数机制或循环垃圾收集器销毁。

PHP请求关闭在php_request_shutdown函数中处理。关闭期间的第一步是调用已注册的关闭功能,然后释放它们。如果其中一个关闭函数持有对某个对象的最后一个引用(或者如果关闭函数本身是一个对象,例如一个闭包),这显然也会导致对象被破坏。

关闭函数运行后,下一步是您感兴趣的一步:PHP将运行zend_call_destructors,然后调用shutdown_destructors。此函数将(尝试)分三步调用所有析构函数:

  1. 首先,PHP会尝试销毁全局符号表中的对象。这种情况发生的方式相当有趣,所以我复制了下面的代码:

    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)));
    

    zend_hash_reverse_apply函数将向后移动符号表 ,即从最后创建的变量开始,然后转向首先创建的变量。在行走时,它将使用refcount 1销毁所有对象。执行此迭代,直到不再使用它来销毁其他对象。

    所以这基本上做的是a)删除全局符号表中的所有未使用的对象b)如果有新的未使用的对象,也删除它们c)等等。使用这种破坏方式,因此对象可以依赖于析构函数中的其他对象。这通常可以正常工作,除非全局范围内的对象具有复杂(例如循环)的相互关系。

    全局符号表的销毁与所有其他符号表的销毁明显不同。通常,符号表是通过将它们前进并仅在所有对象上删除refcount来销毁的。另一方面,对于全局符号表,PHP使用更智能的算法来尝试尊重对象依赖性。

  2. 第二步是调用所有剩余的析构函数:

    zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    

    这将遍历所有对象(按创建顺序)并调用它们的析构函数。请注意,这只会调用" dtor"处理程序,但不是" free"处理程序。这种区别在内部很重要,基本上意味着PHP只会调用__destruct,但实际上不会销毁该对象(甚至不会更改其引用计数)。因此,如果其他对象引用了dtored对象,它仍然可用(即使已经调用了析构函数)。他们将使用某种"半毁坏"对象,在某种意义上(见下面的例子)。

  3. 如果在调用析构函数时停止执行(例如由于die),则剩余的析构函数被调用。相反,PHP会标记对象已被破坏:

    zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    

    这里的重要教训是,在PHP中,析构函数不一定被称为。发生这种情况的情况相当罕见,但可能会发生。此外,这意味着在此之后将不再调用析构函数,因此(相当复杂的)关闭过程的其余部分不再重要。在关闭期间的某个时刻,所有对象都将被释放,但由于已经调用了析构函数,因此用户空间不会显着。

  4. 我应该指出这是目前的关机顺序。这在过去发生了变化,未来可能会发生变化。这不是你应该依赖的东西。

    使用已经被破坏的对象的示例

    这是一个示例,表明有时可以使用已经调用了析构函数的对象:

    <?php
    
    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) {});
    

    以上脚本will output destructed

答案 1 :(得分:7)

  

解构的确切顺序是什么?任何人都可以详细描述PHP使用的销毁顺序的实现吗?而且,如果这个顺序在任何和所有PHP版本之间不一致,那么任何人都可以查明这个订单改变的PHP版本吗?

我可以用一种稍微迂回的方式为你回答其中的三个。

破坏的确切顺序并不总是很清楚,但在给定单个脚本和PHP版本的情况下始终保持一致。也就是说,使用相同参数运行的相同脚本以相同的顺序创建对象基本上总是会获得相同的销毁顺序,只要它在相同的PHP版本上运行。

关闭进程 - 在脚本执行停止时触发对象销毁的事情 - 最近已经发生了变化,至少两次以间接影响破坏顺序的方式发生了变化。其中一个在我必须维护的一些旧代码中引入了错误。

重要人物回​​到了5.1。在5.1之前,用户的会话在关闭序列的最开始,在对象销毁之前写入磁盘。这意味着会话处理程序可以访问任何遗留在对象上的内容,例如自定义数据库访问对象。在5.1中,会话在一次对象销毁之后写入。为了保留以前的行为,您必须手动注册一个关闭函数(在销毁之前,在关闭开始时按照定义的顺序运行),以便在写入例程时成功写入会话数据需要一个(全局)对象。

目前尚不清楚5.1更改是打算还是错误。我见过两人都声称。

下一次更改是在5.3中,引入了the new garbage collection system。虽然关机时的操作顺序保持不变,但精确的破坏顺序现在可以根据引用计数和其他令人愉快的恐怖而改变。

NikiC's answer详细介绍当前(在撰写本文时)关闭过程的内部实施。

再一次,这在任何地方都无法保证,文档非常明确地告诉您永远不要假设销毁订单