Циклические ссылки и сборщик мусора

2 июня 2018
Циклические ссылки возникают, когда элемент массива содержит ссылку на родительский массив, а ещё когда свойство объекта содержит ссылку на сам объект. Если в структуре (массив, объект) присутствует циклическая ссылка и эта структура удаляется, то до версии PHP 5.3 фактического освобождения памяти переменной не происходило, так как структура значения zvav в поле refcount__gc (количество переменных, ссылающихся на участок памяти) содержала не нулевое число. Циклическая ссылка адресовалась по удаляемому значению и значит ячейка считалась используемой, соответственно refcount__gc > 0, а освобождение происходит только для refcount__gc = 0. Из-за этого происходило неконтролируемое захламление свободной оперативной памяти и возникала проблема утечки памяти.

С версией PHP 5.3 был внедрён механизм сбора циклических ссылок (https://www.php.net/manual/ru/features.gc.collecting-cycles.php), решающий проблему утечки памяти.

Следующий код запускает цикл в 30 001 шаг и на каждом шаге создаётся массив, у которого второй элемент ссылается на родительский массив, создавая циклическую ссылку. Затем массив уничтожается функцией unset. На нулевом и далее на каждом 2500-ом шаге выводится количество занимаемой скриптом оперативной памяти.
//Использовано памяти на текущий момент
$current_memory_usage = memory_get_usage();

for($i=0; $i <= 30000; $i++)
{
	$a = array(
		1 => 'Тест',
		2 => null,
	);
	$a[2] = &$a; //Элемент 2 ссылаем на массив
	unset($a); //Удаляем переменную

    if($i % 2500 == 0)
    {
        echo '<span style="color: #999;">Шаг '.$i.'.</span> Использовано памяти: '.ceil((memory_get_usage() - $current_memory_usage)/1024)." Кб<br>";
    }
}

Как мы видим, сначала память увеличивается, а затем резко уменьшается. И так несколько раз
Шаг 0. Использовано памяти: 1 Кб
Шаг 2500. Использовано памяти: 978 Кб
Шаг 5000. Использовано памяти: 1954 Кб
Шаг 7500. Использовано памяти: 2931 Кб
Шаг 10000. Использовано памяти: 1 Кб
Шаг 12500. Использовано памяти: 978 Кб
Шаг 15000. Использовано памяти: 1954 Кб
Шаг 17500. Использовано памяти: 2931 Кб
Шаг 20000. Использовано памяти: 1 Кб
Шаг 22500. Использовано памяти: 978 Кб
Шаг 25000. Использовано памяти: 1954 Кб
Шаг 27500. Использовано памяти: 2931 Кб
Шаг 30000. Использовано памяти: 1 Кб

Такое поведение памяти происходит из-за особенности работы сборки циклических ссылок. Когда счётчик ссылок refcount__gc уменьшается (при разрыве связи со ссылкой и в случае удаления переменной), сборщик помечает потенциальную циклическую структуру zvav специальным буферным флагом, что означает нахождение структуры в корневом буфере. В терминологии алгоритма сборки содержимое буфера называется корнями. По факту наполнения буфера корнями в количестве 10 000 штук запускается сборщик мусора. Объём буфера настраивается в исходных кодах PHP в константе GC_ROOT_BUFFER_MAX_ENTRIES.

Обходя буфер, сборщик проверяет является ли корень мусорным или это живое использующееся значение. Проверка осуществляется по алгоритму, описанному в докладе «Concurrent Cycle Collection in Reference Counted Systems», представленный в 2001 году на Европейской конференции по объектно-ориентированному программированию.

Функции управления сборкой циклических ссылок