Как PHP управляет памятью и почему сборщик мусора не спит

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему мы вообще задумываемся о памяти в PHP?

Большинство разработчиков на PHP привыкли, что память — это бесконечный ресурс. Запросил массив на миллион элементов? Пожалуйста. Создал объект, потом забыл про него? Ну и ладно, PHP сам подчистит. Но когда ваш проект начинает падать с ошибкой Allowed memory size exhausted или неожиданно тормозить на ровном месте, приходит время разобраться, что же на самом деле происходит под капотом.

Сегодня мы заглянем в чёрный ящик под названием «Память и сборщик мусора в PHP». Вы узнаете, как PHP считает ссылки, почему циклические ссылки — это зло, и как заставить сборщик мусора работать на вас, а не против вас. Поехали!

Как PHP управляет памятью: от запроса до ответа

В отличие от C или C++, где вы сами выделяете и освобождаете память, PHP работает через Zend Engine. У каждого процесса PHP есть свой пул памяти. Когда вы пишете:

$data = [1, 2, 3];

PHP выделяет память под массив, сохраняет значения и увеличивает так называемый refcount (счётчик ссылок). Пока на переменную $data кто-то ссылается, память не освобождается.

Но как только вы делаете:

unset($data);

счётчик ссылок падает до нуля, и Zend Engine мгновенно возвращает память обратно в пул. Всё просто и быстро. Однако есть нюансы…

Ссылочная арифметика: refcount и is_ref

Каждая переменная в PHP — это структура zval, которая хранит значение, тип, флаг is_ref (является ли ссылкой) и refcount. Давайте посмотрим на примере:

$a = "Hello";
$b = $a;  // refcount увеличивается до 2 (копирование при записи)
$b = "World"; // теперь $a и $b — разные zval-ы, refcount для "Hello" падает до 1

Это называется copy-on-write (копирование при записи). Пока вы не меняете значение, PHP экономит память, используя одну и ту же область. Но как только вы изменяете переменную, создаётся копия. Хитро, правда?

Сборщик мусора (GC): когда refcount не спасает

Обычный механизм подсчёта ссылок работает отлично для 99% случаев. Но есть одна проблема — циклические ссылки. Представьте, что у вас есть два объекта, которые ссылаются друг на друга:

class Node {
    public $next;
}

$a = new Node();
$b = new Node();
$a->next = $b;
$b->next = $a;

unset($a);
unset($b);

После unset оба объекта всё ещё имеют refcount = 1 (каждый ссылается на другой). Формально они недоступны из кода, но память не освобождается. Это и есть циклическая ссылка.

Если таких объектов много, память будет утекать. Для борьбы с этим в PHP начиная с версии 5.3 появился полноценный сборщик мусора (GC).

Как работает GC в PHP?

Сборщик мусора не запускается на каждую строчку кода. Он активируется, когда корневой буфер (массив потенциальных циклических ссылок) заполняется. По умолчанию его размер — 10 000 элементов. Когда буфер полон, GC запускает алгоритм, который:

  • Сканирует корни — находит все zval-ы с refcount > 0, которые могли бы быть частью цикла.
  • Уменьшает refcount для всех дочерних элементов (как будто ссылок нет).
  • Проверяет — если после уменьшения refcount стал равен нулю, значит это «мусор» и его можно удалить.
  • Восстанавливает refcount для тех, кто не оказался мусором, и освобождает память.

Этот процесс называется garbage collection. Вы можете принудительно запустить его:

gc_collect_cycles();

Но не делайте этого на каждый чих — это дорогая операция.

Как отслеживать утечки памяти в PHP?

Если ваш скрипт работает долго (например, воркер очереди или демон), утечки становятся критичными. Вот несколько инструментов и приёмов:

1. Функции memory_get_usage и memory_get_peak_usage

Самый простой способ — замерить потребление до и после:

echo "Начало: " . memory_get_usage() . " байт\n";
// ваш код
echo "Конец: " . memory_get_usage() . " байт\n";
echo "Пик: " . memory_get_peak_usage() . " байт\n";

2. Используйте xdebug и XHProf

Xdebug может показывать профилирование памяти, а XHProf — строить граф вызовов. На продакшене лучше не включать, но на стейджинге — самое то.

3. Проверяйте циклические ссылки вручную

Если подозреваете, что где-то зациклились объекты, можно использовать:

gc_enabled(); // проверяет, включён ли GC
gc_status(); // возвращает статистику (в PHP 7.3+)

Оптимизация памяти: практические советы

Ниже — проверенные способы не допустить «поедания» памяти вашим кодом:

Используйте unset для временных данных

Особенно в циклах. Если вы обрабатываете CSV-файл на 100 000 строк, не держите все строки в одном массиве:

$handle = fopen('big.csv', 'r');
while (($row = fgetcsv($handle)) !== false) {
    processRow($row);
    unset($row); // явно освобождаем память
}
fclose($handle);

Избегайте глобальных переменных-монстров

Глобальные переменные живут до конца скрипта. Если вы складываете в $GLOBALS['data'] тонны данных, GC не сможет их убрать раньше времени. Лучше используйте локальные переменные или замыкания.

Работайте с генераторами

Генераторы (yield) не хранят всю коллекцию в памяти. Это спасение для больших наборов данных:

function getRows($file) {
    $handle = fopen($file, 'r');
    while (($row = fgetcsv($handle)) !== false) {
        yield $row;
    }
    fclose($handle);
}

foreach (getRows('big.csv') as $row) {
    // обрабатываем по одной строке
}

Следите за статическими свойствами

Статические свойства классов живут в памяти, пока загружен класс. Если вы храните в них результаты запросов или большие конфиги, будьте осторожны. Пример плохого кода:

Блоги

Рекомендаций по книгам