重用对象不会调用`__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
时。
过程可以解释如下(简化):
- 您调用
Bar()
函数创建 FOO
的实例。创建一个对象并将其分配给 $foo
。 (ref_count = 1
)
- 在
Bar()
结束时,对该对象的唯一引用丢失 (ref_count = 0
),这意味着 PHP 开始销毁对象的过程。
2.1.析构函数被调用。在析构函数内部,将 ref_count
增加到 1.
2.2.下一步是让 GC 收集对象,但 ref_count
不为零。这可能意味着一个循环,或者在您的示例中,在析构函数中创建了一个新引用。 GC 必须等到没有非循环引用才能收集对象。
- 您再次拨打
Bar()
。你将同一个对象从静态数组中移出,这意味着 FOO::$pool
内部的引用消失了,但你立即将其分配给 $foo
。 (ref_count = 1
)
- 在
Bar()
结束时,对该对象的唯一引用丢失了(ref_count = 0
),这意味着GC终于可以回收该对象了。析构函数已经被调用,所以这里不需要采取其他行动。
你没有回收对象,你只是延长了它在内存中的存在时间。您可以访问它更长的时间,但是 PHP 该对象已经处于被销毁的过程中。
我试图在 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
时。
过程可以解释如下(简化):
- 您调用
Bar()
函数创建FOO
的实例。创建一个对象并将其分配给$foo
。 (ref_count = 1
) - 在
Bar()
结束时,对该对象的唯一引用丢失 (ref_count = 0
),这意味着 PHP 开始销毁对象的过程。
2.1.析构函数被调用。在析构函数内部,将ref_count
增加到 1.
2.2.下一步是让 GC 收集对象,但ref_count
不为零。这可能意味着一个循环,或者在您的示例中,在析构函数中创建了一个新引用。 GC 必须等到没有非循环引用才能收集对象。 - 您再次拨打
Bar()
。你将同一个对象从静态数组中移出,这意味着FOO::$pool
内部的引用消失了,但你立即将其分配给$foo
。 (ref_count = 1
) - 在
Bar()
结束时,对该对象的唯一引用丢失了(ref_count = 0
),这意味着GC终于可以回收该对象了。析构函数已经被调用,所以这里不需要采取其他行动。
你没有回收对象,你只是延长了它在内存中的存在时间。您可以访问它更长的时间,但是 PHP 该对象已经处于被销毁的过程中。