【问题标题】:Reused object will not call `__destruct`?重用的对象不会调用`__destruct`?
【发布时间】:2020-04-16 03:55:22
【问题描述】:

我试图在 CLI 程序中创建一个类似“池”的结构,其中包括大量“借用”和“回收”。而在测试的时候,我遇到了一些非常出乎意料的事情:

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

这里可以看到,当一个对象被“重用”时(见例1中的2911和1038,例2中的7278),它的__destruct()不会被调用;相反,它会简单地消失。

我很困惑。为什么会发生这种情况?为什么FOO-&gt;__destruct 不会每次都被调用?是否可以让FOO 实例自动回收?

我正在 PHP-7.2 中对此进行测试,在 Windows 和 WSL-Ubuntu 中都观察到了这种行为。

【问题讨论】:

    标签: php destructor


    【解决方案1】:

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

    起初,当我阅读您的问题时,我担心您造成了内存泄漏,但事实并非如此。只要您离开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,该对象已经在被销毁的过程中。

    【讨论】:

    • 正要发布类似的内容,但这是经过深思熟虑的。优秀的答案。只是要确定;如果Bar() 被调用了奇数次,GC 只会在脚本完成时收集最后一个实例,对吗?由于该静态属性中的 ref_count 将保持为 1。
    • 没错。在脚本执行结束时,FOO 内部的静态数组将被销毁,并且其中引用的对象也将被收集。然而,这是一个有争议的问题,因为脚本结束时将释放所有内存。
    • 对,这本身不是问题,只是想确保我得到了正确的部分。谢谢。
    • 好的,很酷,我已经想到但从未“想要”确认的事情。我想我应该坚持手动回收。谢谢!
    猜你喜欢
    • 2010-11-30
    • 2011-10-22
    • 2015-03-12
    • 1970-01-01
    • 2019-06-06
    • 2011-02-16
    • 2017-01-25
    • 1970-01-01
    • 2019-09-10
    相关资源
    最近更新 更多