[php-src] Issue #9239: Garbage collector not triggered on large number of roots
| From: | olsavmic | Date: | Wed, 03 Aug 2022 09:07:58 +0000 |
| Subject: | [php-src] Issue #9239: Garbage collector not triggered on large number of roots | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-242111@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/9239
Author: olsavmic
### Description
The following code:
```php
<?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