I was trying to make a "pool" like structure in a CLI program, which includes lots of "borrowing" and "recycling". And while testing things, I encountered something quite unexpected:
<?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";
The output is:
Bar
Got 2911
Recycling 2911
Bar
Got 2911
Bar
Got 1038
Recycling 1038
Bar
Got 1038
Array
(
)
End.
If I limit the Bar()
call to 3 times instead of 4, I got the following output:
Bar
Got 7278
Recycling 7278
Bar
Got 7278
Bar
Got 6703
Recycling 6703
Array
(
[0] => FOO Object
(
[num] => 6703
)
)
End.
Here you can see, when an object is "reused" (see 2911 and 1038 in example 1, 7278 in example 2), its __destruct()
will not be called; instead, it will simply disappear.
I'm confused. Why would this happen? Why wouldn't FOO->__destruct
got called every time? And is it possible to make instances of FOO
auto recycle itself?
I'm testing this in PHP-7.2, behavior observed both in Windows and WSL-Ubuntu.
As far as I know destructors will not be called twice for the same object. It is generally a bad practice to reuse objects from the destructor. An object whose destructor was called should be destroyed, not reused. In your test script it doesn't cause any serious issues, but in real life, such usage could lead to unexpected behaviours if the developer is not careful.
At first when I read your question, I was worried that you have created a memory leak, but that is not the case. The destructor is called as soon as you leave the scope of Bar()
only if it hasn't been called yet on this object. However, because you save the reference and increase the ref_count
of this object inside of the __destruct()
the garbage collector cannot collect the object yet. It must wait until the next time when you decrease the ref_count
of this object.
The process can be explained as follows (simplified):
Bar()
function which creates an instance of FOO
. An object is created and assigned to $foo
. (ref_count = 1
)Bar()
the only reference to this object is lost (ref_count = 0
), which means PHP starts the process of destroying the object.ref_count
to 1.ref_count
is not zero. This could mean a cycle or like in your example, a new reference has been created in the destructor. GC must wait until there are no uncyclical references to collect the object.Bar()
again. You shift the same object from static array, which means that the reference inside of FOO::$pool
is gone, but you immediately assign it to $foo
. (ref_count = 1
)Bar()
the only reference to this object is lost (ref_count = 0
), which means GC can finally collect this object. The destructor has already been called, so there is no other action to be taken here.You didn't recycle the object, you merely prolonged its existance in the memory. You could access it for a little longer, but for PHP that object was already in the process of being destroyed.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments