Утечки памяти в PHP
12 сентября 2010
Утечки памяти обычно не беспокоят PHP-разработчиков. Типичное приложение обрабатывает один запрос и работает не больше секунды. После этого вся использованная им память освобождается. Даже если приложение кушает слишком много, максимум, разработчик упирается в memory_limit
, выставленный хостером, что решить в общем случае довольно просто: как только переменная становится не нужна, очищаем память, занимаемую ей, при помощи unset
.
Однако, при выполнении ресурсоёмких задач (например, обработки большого количества данных) или запуске PHP как демона проблема утечек встаёт очень остро.
В PHP 5.2 нет полноценного сборщика мусора. Вместо него используется подсчёт ссылок.
Все значения переменных хранятся в памяти. И чтобы занимать как можно меньше места, переменные с одинаковыми значениями просто ссылаются на одну и ту же область памяти. При этом количество ссылок подсчитывается и, как только оно становится равно нулю, память освобождается.
$a = 10; // выделяем область в памяти, одна ссылка $b = $a; // две ссылки unset($a); // одна ссылка
$a = 10; // выделяем область в памяти, одна ссылка $b = $a; // две ссылки $b = 1; // выделяем вторую область в памяти под значение 1, одна ссылка на 1, одна ссылка на 10
В PHP 5.2 причиной утечек являются циклические ссылки:
class A { private $b; function __construct(){ $this->b = new B($this); } } class B { private $a; function __construct($a){ $this->a = $a; } } $i=1; while($i<=1000){ $a = new A(); // 1 ссылка на A ($a). // 1 ссылка на B (A::$b). // 2 ссылки на A (B::$a). unset($a); // 1 ссылка на A всё ещё осталась. Память освобождать рановато. echo $i."\t".memory_get_usage()."\n"; $i++; }
Исправляется это явным уничтожением ссылки на B при помощи unset:
class A { private $b; function __construct(){ $this->b = new B($this); } function __destruct(){ unset($this->b); } } class B { private $a; function __construct($a){ $this->a = $a; } } $i=1; while($i<=1000){ $a = new A(); // 1 ссылка на A ($a). // 1 ссылка на B (A::$b). // 2 ссылки на A (B::$a). unset($a); // 1 ссылка на A (минус одна в B::__destruct). // 0 ссылок на A (unset). Память можно освободить. echo $i."\t".memory_get_usage()."\n"; $i++; }
В PHP 5.3 более умный сборщик мусора, который умеет находить и подчищать последствия использования циклических ссылок. Однако, поиск таких ссылок занимает значительное время и зависит от количества «неподчищенных» ссылок. Плюс к этому работает сборщик не постоянно, а срабатывает только при наполнении буфера ссылок. То есть до его срабатывания какое-то количество памяти всё-таки успевает утекать.
На заметку. Посмотреть, сколько памяти кушает ваше приложение можно при помощи следующих функций:
memory_get_usage()
— использованная скриптом память в байтах в момент вызова функции.memory_get_usage(true)
— использованная скриптом и менеджером памяти PHP память в байтах в момент вызова функции.memory_get_peak_usage()
— максимальное количество памяти в байтах, использованной скриптом с запуска скрипта до момента вызова функции.memory_get_peak_usage(true)
— максимальное количество памяти в байтах, использованной скриптом и менеджером памяти PHP с запуска скрипта до момента вызова функции.
Комментарии RSS по email OK
Сборщик мусора работает так же, как и в IE для JS, лечение аналогично :)
К сожалению, php течёт не только из-за циклических ссылок. Подтекают некоторые встроенные функции (у меня была проблема с strtotime), текут расширения. Кстати расширения обладают "фичей" и могут грызть память не от текущего процесса, а из shared memory или даже апача. и здесь кроется проблема - вышеописанные функции не показывают эту память, нужно пользоваться утилитами OS а-ля top.
тоже таким занимался таким, правда это было побочным продуктом паттерна регистр http://mabp.kiev.ua/2008/04/17/pattern-registry/
посмотри может пригодиться, например работа с одиночками
AmdY
Да, к сожалению, это так, хоть и не так много встроенного течёт. Я описал то, что реально устранить самостоятельно без патчей к самому PHP.
Спасибо за подробный обзор и полезные ссылки. Столкнулся с серьезными утечками в системе расчета написнной как CLI приложение на ZendFramwork'е, особенно Zend_Db и конкретно Zend_Db_Table Relationships текли. Проблема решилась переходом с 5.2 на 5.3, все таки там этот механизм был значительно улучшен. Обычные unset'ы не помогали (
Если работаете с Yii, будьте внимательны при созданием моделей в цикле. Behaviors как раз и будут оставаться такими подвешенными ссылками. Их надо отцеплять вручную через detachBehavior() в конце итерации.
С PHP 5.3 не проверял на сколько эффективно будет очищаться память в этом случае.
Exel
Да, в Yii поведения подтекают. Тут, к сожалению, пока придумать ничего не удалось. С PHP 5.3 будет чистится ступеньками, как показано в мануале PHP по ссылкам в заметке.
Для 5.3 можно вызвать сборщик когда угодно http://www.php.net/manual/ru/features.gc.php
Во-первых, вместо unset лучше использовать
особенно в объекте.
Во-вторых, "Исправляется это явным уничтожением ссылки на B при помощи unset". Вы вызываете unset в деструкторе, вам не кажется это странным? Ведь если вызван деструктор, то нафиг не надо очищать свойства объекта - они и так очистятся.
А вот если бы был вызван какой-нить метод, в котором мы явно делаем для свойства unset - тогда циклические ссылки нам были бы не страшны. Тогда можно было бы делать unset и переменной, которая ссылается на объект.
Выполнял скрипт (там где Class A и B), тестировал на PHP 5.2.17 (cli) (built: Jan 6 2011 17:28:41) Zend Engine v2.2.0 в 1-м скрипте memory_get_usage() выдает в начале: 59880 в конце: 452176
во 2-м скрипте memory_get_usage() выдает в начале: 60440 в конце: 452744
как увидеть утечку?