重用对象不会调用`__destruct`?

Reused object will not call `__destruct`?

我试图在 CLI 程序中创建一个类似于 "pool" 的结构,其中包括很多 "borrowing" 和 "recycling"。在测试的时候,我遇到了一些意想不到的事情:

<?php
class FOO
{
    public static $pool=[];
    public static function get()
    {
        if(empty(self::$pool))
        {
            self::$pool[]=new self(mt_rand(1000,9999));
        }
        return array_shift(self::$pool);
    }
    protected static function recycle(FOO $foo)
    {
        echo "Recycling {$foo->num}\n";
        self::$pool[]=$foo;
    }

    public $num;
    protected function __construct(int $num)
    {
        $this->num=$num;
    }
    public function __destruct()
    {
        static::recycle($this);
    }
}
function Bar()
{
    $foo=FOO::get();
    echo "Got {$foo->num}\n";
}
echo "Bar\n";
Bar();
echo "Bar\n";
Bar();
echo "Bar\n";
Bar();
echo "Bar\n";
Bar();
print_r(FOO::$pool);
echo "End.\n";

输出为:

Bar
Got 2911
Recycling 2911
Bar
Got 2911
Bar
Got 1038
Recycling 1038
Bar
Got 1038
Array
(
)
End.

如果我将 Bar() 调用限制为 3 次而不是 4 次,我得到以下输出:

Bar
Got 7278
Recycling 7278
Bar
Got 7278
Bar
Got 6703
Recycling 6703
Array
(
    [0] => FOO Object
        (
            [num] => 6703
        )

)
End.

这里可以看到,当一个对象是"reused"时(见例1中的2911和1038,例2中的7278),它的__destruct()不会被调用;相反,它只会消失。

我很困惑。为什么会这样?为什么 FOO->__destruct 不会每次都被调用?是否可以使 FOO 的实例自动回收自身?

我正在 PHP-7.2 中对此进行测试,在 Windows 和 WSL-Ubuntu.

中都观察到了行为

据我所知,不会为同一个对象调用两次析构函数。从析构函数中重用对象通常是一种不好的做法。调用析构函数的对象应该被销毁,而不是重用。在您的测试脚本中,它不会导致任何严重问题,但在现实生活中,如果开发人员不小心,这种用法可能会导致意外行为。

起初看到你的问题时,我担心你造成了内存泄漏,但事实并非如此。只要您离开 Bar() 的范围,就会调用析构函数,前提是尚未在此对象上调用它。但是,因为您保存了引用并在 __destruct() 内增加了该对象的 ref_count,所以垃圾收集器还不能收集该对象。它必须等到下次您减少此对象的 ref_count 时。

过程可以解释如下(简化):

  1. 您调用 Bar() 函数创建 FOO 的实例。创建一个对象并将其分配给 $foo。 (ref_count = 1)
  2. Bar() 结束时,对该对象的唯一引用丢失 (ref_count = 0),这意味着 PHP 开始销毁对象的过程。
    2.1.析构函数被调用。在析构函数内部,将 ref_count 增加到 1.
    2.2.下一步是让 GC 收集对象,但 ref_count 不为零。这可能意味着一个循环,或者在您的示例中,在析构函数中创建了一个新引用。 GC 必须等到没有非循环引用才能收集对象。
  3. 您再次拨打Bar()。你将同一个对象从静态数组中移出,这意味着 FOO::$pool 内部的引用消失了,但你立即将其分配给 $foo。 (ref_count = 1)
  4. Bar()结束时,对该对象的唯一引用丢失了(ref_count = 0),这意味着GC终于可以回收该对象了。析构函数已经被调用,所以这里不需要采取其他行动。

你没有回收对象,你只是延长了它在内存中的存在时间。您可以访问它更长的时间,但是 PHP 该对象已经处于被销毁的过程中。