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

bob*_*yer 25 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();
Run Code Online (Sandbox Code Playgroud)

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

test1
test2
Run Code Online (Sandbox Code Playgroud)

PHP手册是模糊的(重点煤矿突出不确定度):"为有特定对象的任何其它引用的析构函数方法将被立即调用,或者在关断期间,任何顺序 ".

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

Nik*_*kiC 36

首先,这里介绍一般的对象销毁顺序: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)));
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    全局符号表的销毁与所有其他符号表的销毁明显不同.通常,通过向前移动符号表来破坏符号表,只需在所有对象上删除引用计数.另一方面,对于全局符号表,PHP使用更智能的算法来尝试尊重对象依赖性.

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

    zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

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

<?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) {});
Run Code Online (Sandbox Code Playgroud)

上面的脚本将输出destructed.


Cha*_*les 8

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

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

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

关闭过程-即当触发执行脚本已经停止对象破坏的东西- 已经在最近的过去的方式,间接地影响了破坏秩序改变,至少两次.这两个中的一个在我必须维护的一些旧代码中引入了错误.

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

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

随着新垃圾收集系统的引入,下一次改变是在5.3中.虽然停机时的操作顺序保持不变,但破坏的确切顺序现在可以根据引用计数和其他令人愉快的恐怖而改变.

NikiC的答案详细介绍了当前(在撰写本文时)关闭过程的内部实施.

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