Сбор циклических ссылок

Обычно механизмы подсчёта ссылок в памяти, например, используемый в PHP ранее, не решают проблему утечки памяти из-за циклических ссылок. Начиная с версии 5.3.0, в PHP реализован синхронный механизм из исследования "» Concurrent Cycle Collection in Reference Counted Systems", в котором рассматривается этот вопрос.

Полное описание работы алгоритма выходит за рамки данного раздела, поэтому приведены только основы. Прежде всего мы должны задать несколько основных правил. Если счётчик ссылок увеличивается, то контейнер всё ещё используется и не является мусором. Если счётчик уменьшается до нуля, то zval может быть удалён. Исходя из этих правил утечки памяти с циклическими ссылками могут получиться только при уменьшении счётчика ссылок до ненулевого значения. Затем, в выделенных контейнерах можно найти мусор проверив возможность уменьшения всех счётчиков ссылок на единицу и определив те контейнеры, у которых счётчик станет равным нулю.

Алгоритм сборки мусора

Для избежания постоянной проверки на мусор с циклическими ссылками при каждом уменьшении счётчика ссылок, алгоритм добавляет все возможные корни (zval контейнеры) в "корневой буфер" (помечая их как "фиолетовые"). Это также гарантирует попадание любого корня в буфер только один раз. Механизм сборки мусора стартует только тогда, когда наполняется буфер (смотрите шаг A на рисунке выше).

На шаге B алгоритм производит поиск в глубину по всем возможным корням для однократного уменьшения счётчика ссылок на единицу у всех контейнеров (помечая их как "серые"). На шаге C алгоритм снова производит поиск в глубину для проверки счётчиков ссылок. Если он находит счётчик с нулевым значением, то контейнер помечается как "белый" (на рисунке отображено синим). Если же счётчик больше нуля, то происходит поиск в глубину от этого контейнера с обратным увеличением счётчиков на единицу и повторной пометкой как "чёрный" на их контейнерах. На последнем шаге D алгоритм проходит по корневому буферу и удаляет из него корни контейнеров, заодно проверяя какие контейнеры помечены как "белые". Эти контейнеры будут освобождены из памяти.

Теперь, когда вы имеете представление о работе алгоритма, рассмотрим его интеграцию в PHP. По умолчанию сборщик мусора всегда включён. Для изменения этой опции используется параметр zend.enable_gc в php.ini.

Если сборщик мусора включён, алгоритм поиска циклических ссылок выполняется каждый раз, когда корневой буфер наполняется 10,000 корнями (вы можете поменять это значение, изменив константу GC_THRESHOLD_DEFAULT в файле Zend/zend_gc.c в исходном коде PHP и пересобрав PHP). Если сборщик мусора выключен, алгоритм никогда не будет запущен. Тем не менее, буфер всегда заполняется корнями.

Если буфер заполнился при выключенном механизме сборки мусора, то другие корни не будут в него записаны. Таким образом, если они окажутся мусором с циклическими ссылками, то никогда не будут очищены и создадут утечку памяти.

Причиной постоянной записи корней в буфер даже при выключенном механизме сборки мусора является то, что это намного быстрее, чем постоянно проверять включён ли механизм сборки мусора или нет. Однако, сама сборка мусора и алгоритм её анализа могут занимать значительное время.

Помимо изменения параметра zend.enable_gc, механизм сборки мусора также можно запустить и остановить вызвав функции gc_enable() и gc_disable() соответственно. Вызов этих функций имеет тот же эффект, что и включение/выключение механизма с помощью настроек конфигурации. Кроме того, можно запустить сборку мусора, даже если корневой буфер ещё не заполнен. Для этого вы можете вызвать функцию gc_collect_cycles(), которая также возвращает количество циклических ссылок собранных алгоритмом.

Причиной включения и выключения механизма сборки, а также его ручного запуска, может стать то, что некоторые части вашего приложения могут быть требовательными ко времени. В этих случаях вы, возможно, не захотите постороннего вмешательства сборщика мусора. Разумеется, выключая сборщик мусора в определённых местах вашего приложения вы рискуете получить утечку памяти, т.к. потенциально некоторые корни могут не поместиться в ограниченный корневой буфер. Более целесообразно будет вызвать gc_collect_cycles() непосредственно перед вызовом gc_disable() для освобождения памяти и уже записанных корней в буфере. Это очистит буфер и позволит использовать больше места для хранения корней, пока механизм будет выключен.