Управление памятью и сборка мусора
Узнайте, как работают zval, подсчёт ссылок и GC
«Управление памятью и сборка мусора» — бесплатный урок PHP Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Как PHP управляет памятью
Значения PHP хранятся в контейнерах значений, которыми в основном управляет подсчёт ссылок, а сборщик циклических ссылок обнаруживает то, с чем подсчёт ссылок не справляется. Понимание этого объясняет утечки памяти в долгоживущих рабочих процессах, стоимость копирования и ситуации, в которых важен вызов gc_collect_cycles().
Контейнер значения
zval — это контейнер для любого значения PHP: он содержит тип и объединение для хранения значения. Простые типы (int, float, bool) хранятся по значению внутри контейнера и не подсчитываются по ссылкам. Сложные типы (строка, массив, объект) указывают на структуру в куче, для которой ведётся подсчёт ссылок.
<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr; // refcount of the array bumps, no data copy yet (CoW)
// Modifying triggers copy-on-write separation
$copy[] = 4;
var_dump($arr); // still [1,2,3]
var_dump($copy); // [1,2,3,4]
?>Подсчёт ссылок
Каждое значение, для которого ведётся подсчёт ссылок, отслеживает количество контейнеров значений, указывающих на него. Присваивание увеличивает это количество, а удаление переменной или выход из области видимости уменьшает его. Когда количество достигает нуля, память освобождается немедленно — ждать прохода сборщика мусора не нужно. Большая часть памяти PHP освобождается именно так.
<?php
$a = str_repeat('x', 1000); // refcount 1 on the string
$b = $a; // refcount 2
unset($a); // refcount 1
unset($b); // refcount 0 -> freed instantly
echo "Refcounting frees deterministically\n";
?>Копирование при записи
Присваивание не копирует данные: оно делит их и увеличивает счётчик ссылок (копирование при записи). Фактическое дублирование происходит только при первой записи в общее значение. Поэтому передача больших массивов обходится дёшево, пока они не изменяются.
<?php
$big = range(1, 100000);
$shared = $big; // O(1): shared, refcount++
// Reads stay shared and cheap
echo count($shared), PHP_EOL;
// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL; // 1 vs -1
?>Проблема: циклы
Одного подсчёта ссылок недостаточно, чтобы освободить циклические ссылки. Если объект A содержит ссылку на B, а B — обратно на A, их счётчики никогда не достигнут нуля, даже после удаления всех внешних переменных — память остаётся занятой, пока не запустится сборщик циклических ссылок.
<?php
class Node { public ?Node $ref = null; }
$a = new Node();
$b = new Node();
$a->ref = $b; // a -> b
$b->ref = $a; // b -> a (cycle!)
unset($a, $b); // external refs gone, but internal cycle keeps refcount > 0
// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>Сборщик циклических ссылок
Сборщик мусора PHP выполняет синхронную маркировку и очистку корневых объектов. Потенциальные корни циклов (значения, у которых счётчик ссылок уменьшился, но не достиг нуля) помещаются в буфер. Когда буфер корней заполняется (по умолчанию 10 000 элементов), запускается сборка мусора: она моделирует уменьшение счётчиков, находит действительно недостижимые группы и освобождает их.
<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->b = $b; $b->a = $a; // create a cycle
unset($a, $b); // becomes a GC root
}
print_r(gc_status());
?>Управление сборкой мусора
Вы можете управлять сборщиком: gc_disable()/gc_enable() включают и выключают его, а gc_collect_cycles() принудительно запускает проход и возвращает количество освобождённых элементов. В пакетных заданиях, критичных к пропускной способности, иногда отключают сборку мусора на коротком интенсивном этапе, а затем один раз запускают её в конце.
<?php
gc_disable(); // no automatic cycle collection
// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
$x = new stdClass(); $y = new stdClass();
$x->y = $y; $y->x = $x; unset($x, $y);
}
$freed = gc_collect_cycles(); // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>Долгоживущие рабочие процессы
В демонах CLI, потребителях очередей и рабочих процессах долгоживущих серверов память не освобождается после завершения запроса — процесс работает часами. Следите за статическими кэшами, которые растут без ограничений, накапливающимися циклами и незакрытыми ресурсами. Отслеживайте использование памяти с помощью memory_get_usage(true) и периодически перезапускайте рабочие процессы.
<?php
$peakStart = memory_get_usage(true);
// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
$payload = range(0, 10000);
// ... process ...
unset($payload);
}
printf("start: %d KB, peak: %d KB, now: %d KB\n",
$peakStart >> 10,
memory_get_peak_usage(true) >> 10,
memory_get_usage(true) >> 10
);
?>WeakReference и WeakMap
Чтобы кэшировать данные или связывать их с объектом, не удерживая сам объект в памяти, используйте WeakReference (8.0) и WeakMap (8.0). Элемент WeakMap не увеличивает счётчик ссылок ключевого объекта, поэтому объект может быть собран, а его элемент автоматически удалён — это идеально подходит для вспомогательных таблиц метаданных, которые не должны вызывать утечки памяти.
<?php
$map = new WeakMap();
$obj = new stdClass();
$map[$obj] = 'metadata'; // does NOT keep $obj alive
echo count($map) . PHP_EOL; // 1
unset($obj); // object collectible -> entry removed
echo count($map) . PHP_EOL; // 0
?>Интернированные строки и неизменяемые структуры
Литеральные строки в исходном коде интернируются: сохраняются один раз, используются совместно и не подсчитываются по ссылкам и не освобождаются во время запроса. Кэширование опкодов перемещает интернированные строки и неизменяемые кэшированные массивы операций в общую память между запросами. Поэтому копирование строковых литералов практически ничего не стоит.
Лимит памяти и распределитель
PHP использует собственный распределитель памяти, работающий поверх системного распределителя, и учитывает расход памяти каждого запроса в соответствии с memory_limit. При достижении лимита возникает критическая ошибка. Значение true в memory_get_usage(true) показывает фактически выделенные операционной системой блоки, а не только данные, которые в данный момент удерживает Ваш сценарий.
<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n"; // ZendMM tracked
echo 'real usage: ' . memory_get_usage(true) . " bytes\n"; // from OS
?>Быстрая проверка
Почему одного подсчёта ссылок недостаточно и некоторые объекты могут вызывать утечки памяти?
Повторение
PHP хранит значения в контейнерах значений: простые типы передаются по значению, а сложные выделяются в куче и учитываются с помощью подсчёта ссылок и копирования при записи. Подсчёт ссылок детерминированно освобождает память при достижении нулевого значения, но не может освободить циклы, поэтому синхронный сборщик выполняет маркировку и очистку, когда заполняется буфер корней, или по вызову gc_collect_cycles(). Для не владеющих объектом связей используйте WeakMap/WeakReference, а в долгоживущих рабочих процессах следите за неограниченным ростом памяти.
Часто задаваемые вопросы
Урок «Управление памятью и сборка мусора» бесплатный?
Да — полный текст урока «Управление памятью и сборка мусора» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Управление памятью и сборка мусора»?
Узнайте, как работают zval, подсчёт ссылок и GC Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Управление памятью и сборка мусора»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Как работает Zend Engine
- Управление памятью и сборка мусора
- OPcache и JIT-компиляция
- Написание базового расширения PHP на C