-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Description
Description
The following code:
<?php declare(strict_types = 1);
gc_enable();
class Bar
{
public ?Foo $foo = null;
}
class Foo
{
public Bar $bar;
}
function ff()
{
$arr = [];
for ($i = 0; $i < 10000000; $i++) {
$b = new Bar();
$a = new Foo();
$a->bar = $b;
$b->foo = $a;
$arr[] = $a;
}
}
ff();
function memory_usage_mb(): float
{
return memory_get_usage() / 1024 / 1024;
}
echo "GC Status after first run:\n";
var_dump(gc_status());
echo 'Memory usage: ' . memory_usage_mb() . "MB\n";
ff();
echo "GC Status after second run:\n";
var_dump(gc_status());
echo 'Memory usage: ' . memory_usage_mb() . "MB\n";
gc_collect_cycles();
echo "GC Status after gc_collect_cycles:\n";
var_dump(gc_status());
echo 'Memory usage: ' . memory_usage_mb() . "MB\n";
ff();
echo "GC Status after third run:\n";
var_dump(gc_status());
echo 'Memory usage: ' . memory_usage_mb() . "MB\n";
Resulted in this output:
GC Status after first run:
array(4) {
["runs"]=>
int(81)
["collected"]=>
int(19580004)
["threshold"]=>
int(440001)
["roots"]=>
int(209999)
}
Memory usage: 278.76216888428MB
GC Status after second run:
array(4) {
["runs"]=>
int(129)
["collected"]=>
int(38860006)
["threshold"]=>
int(620001)
["roots"]=>
int(569998)
}
Memory usage: 317.21421051025MB
GC Status after gc_collect_cycles:
array(4) {
["runs"]=>
int(130)
["collected"]=>
int(40000000)
["threshold"]=>
int(620001)
["roots"]=>
int(0)
}
Memory usage: 256.33196258545MB
GC Status after third run:
array(4) {
["runs"]=>
int(168)
["collected"]=>
int(58540004)
["threshold"]=>
int(760001)
["roots"]=>
int(729999)
}
Memory usage: 334.30416107178MB
As you can see, escaping from the first ff()
call or calling the ff()
function for the second time did not free the roots of GC even though the GC was supposedly called (the number of runs
increased) although there were more than 209999 roots in the buffer.
Calling manually gc_collect_cycles
seems to clear the root buffer but it actually does not do anything - calling ff()
for the third time actually shows total of 729999 roots in the buffer.
Perhaps this is somehow related to the issue mentioned in https://www.php.net/manual/en/features.gc.collecting-cycles.php
If the root buffer becomes full with possible roots while the garbage collection mechanism is turned off, further possible roots will simply not be recorded. Those possible roots that are not recorded will never be analyzed by the algorithm. If they were part of a circular reference cycle, they would never be cleaned up and would create a memory leak.
The presented case here though has GC enabled all the time, the number of roots seems to be counted and it still leaks memory.
After removing the assignment $arr[] = $a;
from the ff
function, the garbage collector is triggered every 10 000 roots (as documented) and the roots are gone as expected:
GC Status after first run:
array(4) {
["runs"]=>
int(1999)
["collected"]=>
int(19990000)
["threshold"]=>
int(10001)
["roots"]=>
int(10000)
}
Memory usage: 0.99087524414062MB
GC Status after second run:
array(4) {
["runs"]=>
int(3999)
["collected"]=>
int(39990000)
["threshold"]=>
int(10001)
["roots"]=>
int(10000)
}
Memory usage: 0.99087524414062MB
GC Status after gc_collect_cycles:
array(4) {
["runs"]=>
int(4000)
["collected"]=>
int(40000000)
["threshold"]=>
int(10001)
["roots"]=>
int(0)
}
Memory usage: 0.45681762695312MB
I got the same output for PHP 7.4, PHP 8.0, and PHP 8.1.
PHP Version
PHP 8.1.7
Operating System
Debian 10