<rmcreative>

RSS

Утечки памяти в 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

  1. №3091
    Антон Шевчук
    Антон Шевчук 12.09.2010, 9:37:34

    Сборщик мусора работает так же, как и в IE для JS, лечение аналогично :)

  2. №3093
    AmdY
    AmdY 12.09.2010, 18:55:33

    К сожалению, php течёт не только из-за циклических ссылок. Подтекают некоторые встроенные функции (у меня была проблема с strtotime), текут расширения. Кстати расширения обладают "фичей" и могут грызть память не от текущего процесса, а из shared memory или даже апача. и здесь кроется проблема - вышеописанные функции не показывают эту память, нужно пользоваться утилитами OS а-ля top.

  3. №3094
    CTAPbIu_MABP
    CTAPbIu_MABP 12.09.2010, 22:11:36

    тоже таким занимался таким, правда это было побочным продуктом паттерна регистр http://mabp.kiev.ua/2008/04/17/pattern-registry/

    посмотри может пригодиться, например работа с одиночками

  4. №3095
    Sam
    Sam 12.09.2010, 22:48:38

    AmdY

    Да, к сожалению, это так, хоть и не так много встроенного течёт. Я описал то, что реально устранить самостоятельно без патчей к самому PHP.

  5. №3107
    Чистяков Денис
    Чистяков Денис 14.09.2010, 8:14:18

    Спасибо за подробный обзор и полезные ссылки. Столкнулся с серьезными утечками в системе расчета написнной как CLI приложение на ZendFramwork'е, особенно Zend_Db и конкретно Zend_Db_Table Relationships текли. Проблема решилась переходом с 5.2 на 5.3, все таки там этот механизм был значительно улучшен. Обычные unset'ы не помогали (

  6. №3108
    Exel
    Exel 15.09.2010, 10:32:37

    Если работаете с Yii, будьте внимательны при созданием моделей в цикле. Behaviors как раз и будут оставаться такими подвешенными ссылками. Их надо отцеплять вручную через detachBehavior() в конце итерации.

    С PHP 5.3 не проверял на сколько эффективно будет очищаться память в этом случае.

  7. №3110
    Sam
    Sam 15.09.2010, 22:39:00

    Exel

    Да, в Yii поведения подтекают. Тут, к сожалению, пока придумать ничего не удалось. С PHP 5.3 будет чистится ступеньками, как показано в мануале PHP по ссылкам в заметке.

  8. №6050
    psycho-coder
    psycho-coder 14.03.2012, 11:44:40

    То есть до его срабатывания какое-то количество памяти всё-таки успевает утекать.

    Для 5.3 можно вызвать сборщик когда угодно http://www.php.net/manual/ru/features.gc.php

  9. №7988
    Андрей
    Андрей 22.05.2013, 7:47:43

    Во-первых, вместо unset лучше использовать

    $var = null,
    
    

    особенно в объекте.

    Во-вторых, "Исправляется это явным уничтожением ссылки на B при помощи unset". Вы вызываете unset в деструкторе, вам не кажется это странным? Ведь если вызван деструктор, то нафиг не надо очищать свойства объекта - они и так очистятся.

    А вот если бы был вызван какой-нить метод, в котором мы явно делаем для свойства unset - тогда циклические ссылки нам были бы не страшны. Тогда можно было бы делать unset и переменной, которая ссылается на объект.

  10. №8121
    Alex
    Alex 02.07.2013, 23:17:36

    Выполнял скрипт (там где 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

    как увидеть утечку?

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.