Что такое утечка памяти (memory leak) в PHP?

«Что такое утечка памяти (memory leak) в PHP?» — вопрос из категории PHP Core, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Утечка памяти — это ситуация, когда приложение постоянно выделяет память, но не освобождает её, даже когда объекты больше не нужны. Со временем это может исчерпать доступную память и привести к падению скрипта или сервера.

Хотя PHP имеет встроенный сборщик мусора (Garbage Collector, GC), утечки возможны в следующих сценариях:

  1. Циклические ссылки на объектах или массивах: Когда два или более объекта ссылаются друг на друга, создавая изолированный "остров", на который нет внешних ссылок. Старый GC (до PHP 5.3) не мог их очистить. Современный GC (алгоритм подсчета ссылок с циклическим сборщиком) справляется с этим, но не всегда мгновенно.
  2. Глобальные переменные и статические свойства/массивы: Данные, хранящиеся в них, живут до конца выполнения скрипта (для глобальных) или между вызовами (для статических в функциях/методах). Если постоянно добавлять туда данные без очистки — память будет расти.
  3. Расширения PHP: Потенциальные утечки могут быть в некорректно написанных сторонних расширениях на C.

Пример потенциальной проблемы (циклическая ссылка в долгоживущем скрипте):

class Service {
    private array $listeners = [];
    public function addListener(object $listener): void {
        $this->listeners[] = $listener;
        // Если $listener хранит ссылку на этот Service -> циклическая ссылка
    }
}

// В долгоживущем процессе (например, worker ReactPHP или RoadRunner)
$service = new Service();
while (true) {
    $listener = new class($service) {
        private Service $svc;
        public function __construct(Service $svc) {
            $this->svc = $svc; // Создается циклическая ссылка
        }
    };
    $service->addListener($listener);
    // После итерации $listener не уничтожается, т.к. на него ссылается $service->listeners
    // А на $service ссылается $listener->svc
    usleep(1000);
}
// Память будет постепенно расти.

Решение: Для разрыва циклических ссылок можно использовать WeakReference (PHP 7.4+) или явно удалять ссылки (например, $listener->svc = null;). Для глобальных/статических кэшей стоит устанавливать лимиты и использовать TTL.