0Pricing
PHP Academy · Урок

Управление памятью и сборка мусора

Узнайте, как работают 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 — локальная установка не требуется.

Все уроки этого курса

  1. Как работает Zend Engine
  2. Управление памятью и сборка мусора
  3. OPcache и JIT-компиляция
  4. Написание базового расширения PHP на C
← Назад к PHP Academy