Сбор циклических ссылок
Обычно механизмы подсчета ссылок в памяти, например, используемый в 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_ROOT_BUFFER_MAX_ENTRIES
в файле
Zend/zend_gc.c
в исходном коде PHP и пересобрав PHP).
Если сборщик мусора выключен, алгоритм никогда не будет запущен. Тем не менее,
буфер всегда заполняется корнями.
Если буфер заполнился при выключенном механизме сборки мусора, то другие корни не будут в него записаны. Таким образом, если они окажутся мусором с циклическими ссылками, то никогда не будут очищены и создадут утечку памяти.
Причиной постоянной записи корней в буфер даже при выключенном механизме сборки мусора является то, что это намного быстрее, чем постоянно проверять включен ли механизм сборки мусора или нет. Однако, сама сборка мусора и алгоритм ее анализа могут занимать значительное время.
Помимо изменения параметра zend.enable_gc, механизм сборки мусора также можно запустить и остановить вызвав функции gc_enable() и gc_disable() соответственно. Вызов этих функций имеет тот же эффект, что и включение/выключение механизма с помощью настроек конфигурации. Кроме того, можно запустить сборку мусора, даже если корневой буфер еще не заполнен. Для этого вы можете вызвать функцию gc_collect_cycles(), которая также возвращает количество циклических ссылок собранных алгоритмом.
Причиной включения и выключения механизма сборки, а также его ручного запуска, может стать то, что некоторые части вашего приложения могут быть требовательными ко времени. В этих случаях вы, возможно, не захотите постороннего вмешательства сборщика мусора. Разумеется, выключая сборщик мусора в определенных местах вашего приложения вы рискуете получить утечку памяти, т.к. потенциально некоторые корни могут не поместиться в ограниченный корневой буфер. Более целесообразно будет вызвать gc_collect_cycles() непосредственно перед вызовом gc_disable() для освобождения памяти и уже записанных корней в буфере. Это очистит буфер и позволит использовать больше места для хранения корней, пока механизм будет выключен.