28

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

通过测试,我有一个想法:当前范围的 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 手册含糊不清(强调我的以突出不确定性):“只要在关闭序列期间没有对特定对象的其他引用或以任何顺序调用析构函数方法。”

解构的确切顺序是什么?谁能详细描述PHP使用的销毁命令的实现?而且,如果这个顺序在所有 PHP 版本之间不一致,任何人都可以查明哪些 PHP 版本按这个顺序更改?

4

3 回答 3

39

首先,这里介绍了一些关于一般对象销毁顺序的内容: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遍历符号表,即从最后创建的变量开始,然后朝着最先创建的变量前进。在行走时,它将销毁所有引用计数为 1 的对象。执行此迭代,直到没有其他对象被它销毁。

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

    全局符号表的销毁与所有其他符号表的销毁有很大不同。通常,符号表会通过向前移动并删除所有对象的引用计数来破坏。另一方面,对于全局符号表,PHP 使用一种更智能的算法来尝试尊重对象依赖关系。

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

    zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    

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

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

    zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    

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

我应该指出,这是目前的关闭命令。这在过去发生了变化,将来可能会发生变化。这不是你应该依赖的东西。

使用已破坏对象的示例

这是一个示例,显示有时可以使用已调用其析构函数的对象:

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

上面的脚本将输出destructed.

于 2012-12-31T12:43:26.073 回答
9

解构的确切顺序是什么?谁能详细描述PHP使用的销毁命令的实现?而且,如果这个顺序在任何和所有 PHP 版本之间不一致,任何人都可以确定这个顺序在哪些 PHP 版本中发生变化?

我可以以一种有点迂回的方式为你回答其中三个。

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

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

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

目前尚不清楚 5.1 的更改是有意的还是错误的。我见过两者都声称。

下一个变化是在 5.3 中,引入了新的垃圾收集系统。虽然关闭时的操作顺序保持不变,但精确的破坏顺序现在可以根据参考计数和其他令人愉快的恐怖而改变。

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

再一次,这在任何地方都无法保证,并且文档非常明确地告诉您永远不要承担销毁命令

于 2012-12-31T08:01:13.393 回答
0

对于任何有兴趣的人 - 如 PHP 8.0:

class A {
  
  function __destruct() {
    print get_class();
  }
}

class B {
  private $child;

  function __construct() {
    $this->child = new A();
  }
  
  function __destruct() {
    print get_class();
  }
}

class C {
  private $child;

  function __construct() {
    $this->child = new B();
  }
  
  function __destruct() {
    print get_class();
  }
}


new C;

导致输出

CBA

IE。包含对象析构函数在包含对象析构函数之前触发。

如果需要,可以颠倒顺序,即。为 ABC,将除 A(最内层)之外的所有析构函数更改为:

function __destruct() {
  unset($this->child);
  print get_class();
}
于 2021-12-18T13:22:06.813 回答