Сборщик мусора в PHP 5

March 18, 2008, revised August 9, 2012 PHP

Предупреждение! Начиная с PHP 5.3, сборщик мусора удаляет циклические ссылки. Статья сохранена для истории.

Сегодня на работе разбирался с PHPшным сборщиком мусора. Обнаружилась одна жутко неприятная вещь, которая называется recursive reference memory leak - объекты с перекрестными ссылками не удаляются из памяти.

Как известно, объекты PHP 5 реализуются через механизм smart-pointer. Понимание этого механизма вообще нужно для адекватной работы с объектами, так что изложу вкратце. Осторожно, мнение выходца из C++. :)

Что такое smart-pointer? Это такой паттерн, который подсчитывает количество ссылок на себя, и когда оно становится равным нулю, удаляет объект, на который ссылается сам.

Подробнее про самостоятельную реализацию smart-pointer на PHP можно почитать, например, в Advanced PHP Programming.

Конечно, обычно PHP очищает ссылки при выходе из области определения переменной, и память освобождается вовремя.

Поправка: память очистится не сразу, а при ближайшем запуске сборщика мусора.

Но есть одна ситуация, и довольно часто используемая, когда этого не происходит.

class A {
  function __construct () {
    $this->b = new B($this);
  }
}

class B {
  function __construct ($parent = NULL) {
    $this->parent = $parent;
  }
}

while (true) {
  $a = new A();
}

Удаление свойства $a->b происходит после удаления объекта $a (резонно - в деструкторе оно может пригодиться). В деструкторе B объект `$b->parent уже удален, и эта ссылка не очищается. Отсюда утечка памяти.

Пример из жизни

Привожу пример из жизни классической ActiveRecord-модели:

class Record{
  #...
  private $_fields;
  #...
}

class Field {
  #...
  private $_record;
  #...
}
...
while ($record = $result->next()) {
  $record->doSomething();
}

Так вот такой код со страшной силой жрет ОЗУ.

Решение

На данный момент приходится обнулять ссылки вручную:

class A {
  function __construct () {
    $this->b = new B($this);
  }
  function __destruct () {
    $this->b->__destruct();
  }
}

class B {
  function __construct ($parent = NULL) {
    $this->parent = $parent;
  }
  function __destruct () {
    unset($this->parent);
  }
}

[edit] кроме того, вызывать деструктор для A придется вручную - поскольку в ссылка на него остается во вложенном объекте, он не будет удален автоматически.

Багрепорту, кстати, уже два с половиной года: [http://bugs.php.net/bug.php?id=33595](http://bugs.php.net/bug.php?id=33595, и его вроде как закрыли в PHP 5.3.

Одно это делает переход на 5.3 весьма ожидаемым.

UPD 2012-08-09 Да, PHP 5.3 успешно обрабатывает циклические ссылки.

Buy Me a Coffee at ko-fi.com