<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · SENIOR

Як працює garbage collector у PHP і коли він стає проблемою?

PHP звільняє памʼять через підрахунок посилань, а окремий збирач циклів періодично прибирає обʼєкти, які посилаються одне на одного.

Чому воркер черги росте в памʼяті, хоча кожен job невеликий?
Що таке refcount і чому його недостатньо?
Коли в PHP взагалі запускається збирач сміття?
памʼять GC воркери

Основний механізм керування памʼяттю в PHP — підрахунок посилань. Кожен zval знає, скільки змінних чи властивостей на нього вказують; щойно лічильник падає до нуля, памʼять звільняється негайно, без участі збирача. Саме тому у звичайному запиті про GC можна не думати.

Refcount не справляється з циклами. Якщо два обʼєкти тримають один одного, після unset зовнішніх змінних у кожного лишається одне посилання від сусіда, і нуль недосяжний. Для таких випадків існує збирач циклів: можливі корені накопичуються в root buffer, і коли він заповнюється, алгоритм обходить граф і звільняє недосяжні підграфи. Запуск не привʼязаний до часу, а з PHP 7.3 поріг адаптивний.

Проблемою GC стає в довгих процесах: воркери черг, Octane, RoadRunner, демони. Процес живе години, тому все, що не звільнилось, накопичується: цикли між обʼєктами, статичні кеші, реєстри слухачів, identity map ORM. Виглядає це як повільний ріст RSS воркера до OOM.

Лікування починається з діагностики, а не з gc_collect_cycles. Метрики memory_get_usage у логах воркера, ліміт памʼяті й кількості завдань для контрольованого перезапуску, WeakMap для кешів, привʼязаних до обʼєктів, і gc_status для розуміння, чи взагалі збирач знаходить сміття. Якщо не знаходить, витік у ваших статичних структурах, а не в циклах.

final class Node
{
    public ?Node $peer = null;
}

$a = new Node;
$b = new Node;
$a->peer = $b;
$b->peer = $a;      // цикл: refcount кожного = 2

unset($a, $b);      // refcount кожного = 1, памʼять не звільнена
gc_collect_cycles(); // лише збирач циклів прибере обидва

// Типова пастка у довгому воркері: статичний кеш росте вічно
final class Registry
{
    private static array $seen = [];
    public static function remember(object $o): void { self::$seen[] = $o; }
}

// Безпечна альтернатива: WeakMap не тримає обʼєкт живим
$cache = new WeakMap;
$cache[$order] = computeTotals($order); // зникне разом з $order

// Контроль у воркері
if (memory_get_usage(true) > 256 * 1024 * 1024) {
    exit(12); // супервізор перезапустить процес
}
Що основний механізм це refcount у zval: обʼєкт звільняється миттєво, щойно лічильник посилань падає до нуля.
Що refcount не бачить циклів: два обʼєкти, які тримають одне одного, ніколи не досягнуть нуля, тому існує окремий збирач циклів на root buffer.
Що збирач запускається не за таймером, а коли root buffer заповнюється (10 000 можливих коренів), і в PHP 7.3+ поріг адаптивний.
Практику для довгих процесів: gc_collect_cycles у воркері, memory_get_usage у метриках, обмеження max-jobs або memory для перезапуску воркера, обережність зі статичними кешами.
Розуміння, що витік у воркері частіше спричинений не GC, а вашим кодом: статичні масиви, реєстри подій, накопичені слухачі, identity map ORM.
Казати, що PHP звільняє памʼять лише наприкінці запиту: більшість обʼєктів звільняється одразу при refcount = 0.
Вважати, що unset($obj) гарантовано звільняє обʼєкт: якщо на нього є інші посилання або цикл, памʼять лишається.
Плутати збирач циклів з opcache або з памʼяттю Zend MM: memory_get_usage показує аллокатор PHP, а не RSS процесу.
Лікувати витік через gc_collect_cycles на кожен job, не знайшовши, хто саме тримає посилання.
ПОРАДА

Сильна відповідь містить практику: gc_collect_cycles у довгих воркерах, memory_get_usage у метриках і перезапуск воркера після N запитів. Ще краще розповісти про реальний витік, який ви знайшли.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Обʼєкт звільняється, коли refcount падає до нуля, а цикли збирає окремий збирач з root buffer.