Сборщик мусора в PHP 5
Сегодня на работе разбирался с PHPшным сборщиком мусора. Обнаружилась одна жутко неприятная вещь, которая называется recursive reference memory leak – объекты с перекрестными ссылками не удаляются из памяти.
Как известно, объекты PHP 5 реализуются через механизм smart-pointer. Понимание этого механизма вообще нужно для адекватной работы с объектами, так что изложу вкратце. Осторожно, мнение выходца из C++. :)
Что такое smart-pointer? Это такой паттерн, который подсчитывает количество ссылок на себя, и когда оно становится равным нулю, удаляет объект, на который ссылается сам.
Подробнее про самостоятельную реализацию smart-pointer на PHP можно почитать, например, в Advanced PHP Programming.
- Когда создается объект, под него выделяется участок память и smart-pointer. Переменной присваивается ссылка на smart-pointer.
- При копировании объекта, на самом деле, создается еще одна ссылка на smart-pointer.
- При клонировании объекта оператором
cloneсоздается копия памяти и новый smart-pointer с одной ссылкой (той, что новая). - При выполнении
unset($object)удаляется только данная ссылка на объект – если это не последняя ссылка, то объект остается в памяти. (важно!) - При выполнении
$object->__destruct()выполняется деструктор объекта как метод – это тоже не освобождает память! Так же, как иobject.~Class()в C++. Более того, это, возможно, испортит объект – а ссылки на него могли остаться. - Единственный способ удалить объект из памяти – удалить все ссылки на него.
Конечно, обычно PHP очищает ссылки при выходе из области определения переменной, и память освобождается вовремя.
Но есть одна ситуация, и довольно часто используемая, когда этого не происходит.
<?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();
}
Так вот такой код со страшной силой жрет ОЗУ.
Решение
На данный момент приходится обнулять ссылки вручную:
<?php
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, и его вроде как закрыли в PHP 5.3.
Одно это делает переход на 5.3 весьма ожидаемым.

Вы забыли сказать, что деструктор А прийдется вызывать вручную:
while (true) {
}
Спасибо! И правда, автоматически деструктор не вызывается.
Спасибо за такой обзор, мне кажется в интернете слишком мало таких статей (особенно о PHP)
Да в 5.3 появится гарбадж-коллектор что вместе с неймспейсами делает этот релиз таким долгожданным.
А кто сказал, что __destruct() не вызывается автоматически. По-моему он срабатывает в таком варианте unset($a);
Что люди только не сделают, чтобы нормальный язык с нормальным GC не использовать%)
А нормальный язык – это какой?