【问题标题】:In which order are objects destructed in PHP?PHP中对象的破坏顺序是什么?
【发布时间】:2012-12-15 07:36:47
【问题描述】:

对象解构的具体顺序是什么?

通过测试,我有一个想法:当前范围的 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 版本在这个顺序中发生了变化?

【问题讨论】:

  • 如果文档说它可以是任何顺序,那么即使该顺序在您的所有测试和您关心的所有版本中看起来都是稳定的,也不要假设任何特定的顺序关于。为什么你还需要这个?如果它很重要,您应该明确说明它而不是依赖最终确定。顺便说一句,这适用于任何语言。
  • 为什么确切的顺序很重要?如果手册明确指出它是“任意顺序”,则永远不要依赖它。
  • “任何顺序”并不意味着它是随机的,只是不能保证顺序。它仍然可以根据实现细节有一个确定的顺序。
  • 在实践中它不是随机的,但您应该像这样编写代码。理解“任意”一词的良好经验法则。
  • 您是来自 C 或其他一些低级语言,您已经或完全知道某事发生了什么、何时以及如何发生,可能是因为您必须这样做? PHP 不是那样的。更上一层楼,所以这些低级任务完全在内部以一种我们不知道的方式进行管理,就像 GC 一样。当它发生时它就会发生,它背后的工程有时是……不寻常的。老实说,您可能必须找到一个 PHP 源工程师,或者(就像我正在做的那样)尝试找到一个工具来映射流程并直观地描述它们。真的很难说。

标签: php php-internals


【解决方案1】:

首先,这里介绍一下一般的对象销毁顺序: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) 等等。使用这种破坏方式,以便对象可以依赖于析构函数中的其他对象。这通常可以正常工作,除非全局范围内的对象具有复杂的(例如循环)相互关系。

    全局符号表的销毁与所有其他符号表的销毁有很大不同。通常,符号表通过 forward 遍历它们并仅删除所有对象上的 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 中,析构函数不一定被调用。发生这种情况的情况相当罕见,但它可能会发生。此外,这意味着在此之后将不再调用析构函数,因此(相当复杂的)关闭过程的其余部分不再重要。在关闭期间的某个时刻,所有对象都将被释放,但由于已经调用了析构函数,这对于用户空间来说并不明显。

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

使用已破坏对象的示例

下面是一个例子,说明有时可以使用已经调用了析构函数的对象:

<?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.

【讨论】:

  • 谢谢!虽然我理解依赖这种行为是不好的形式,而且大多数 PHP 用户不会遇到这种情况,但我仍然想知道它是如何工作的,以及它是否真的可以预测。此处的此信息将帮助我避免错误并更轻松地隔离错误/错误代码。
  • 使用“破坏”对象的示例帮助我弄清楚了一些遗留代码中发生了什么。一个“包括厨房水槽在内的所有东西”对象都在销毁时渲染,但在它的辅助对象被销毁之后。原来错误处理程序让它一直存活到最后!
  • 然而,在循环引用(以及那些循环引用对象持有/引用的任何对象)的情况下,zend_objects_store_call_destructors 不保证销毁顺序,因为objects_store 中的“槽”可能是重用,顺序根本不是对象创建的顺序。
【解决方案2】:

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

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

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

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

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

尚不清楚 5.1 的更改是有意的还是错误的。我已经看到两者都声称了。

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

NikiC's answer 有关于关闭过程的当前(在撰写本文时)内部实现的详细信息。

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

【讨论】:

  • 哇。谢谢。如果我能接受 NikiC 和你的答案,我会的。我有一种感觉,所有版本的实现都不一样,我想确定一下。会话处理程序肯定会成为这个领域的一个难题,我很高兴那里的手册 (php.net/manual/en/function.session-set-save-handler.php) 提供了警告和解决方法。
【解决方案3】:

对于任何感兴趣的人 - 如 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

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

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

function __destruct() {
  unset($this->child);
  print get_class();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-27
    • 2021-04-20
    • 1970-01-01
    • 2012-12-31
    • 2020-06-12
    • 2015-09-30
    • 2021-08-03
    • 2017-06-13
    相关资源
    最近更新 更多