Диагностика утечек памяти - разрешенный объем памяти исчерпан # байтов

98

Я столкнулся с ужасным сообщением об ошибке, возможно, из-за кропотливых усилий PHP исчерпал память:

Допустимый объем памяти #### байт исчерпан (попытка выделить #### байт) в file.php в строке 123

Повышение лимита

Если вы знаете, что делаете, и хотите увеличить лимит, см. Memory_limit :

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

Осторожно! Возможно, вы решаете только симптом, а не проблему!

Диагностика утечки:

Сообщение об ошибке указывает на строку внутри цикла, который, как я полагаю, вызывает утечку или ненужное накопление памяти. Я напечатал memory_get_usage()операторы в конце каждой итерации и вижу, как число медленно растет, пока не достигнет предела:

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

Для целей этого вопроса давайте предположим, что худший код спагетти, который только можно вообразить, прячется в глобальной области где-то в $userили Task.

Какие инструменты, уловки PHP или отладка voodoo могут помочь мне найти и исправить проблему?

Майк Б.
источник
PS - Я недавно столкнулся с проблемой именно с этим типом вещей. К сожалению, я также обнаружил, что у php есть проблема уничтожения дочерних объектов. Если вы отключите родительский объект, его дочерние объекты не будут освобождены. Необходимо убедиться, что я использую модифицированный unset, который включает рекурсивный вызов всех дочерних объектов __destruct и так далее. Подробности здесь: paul-m-jones.com/archives/262 :: Я делаю что-то вроде: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ элемент -> __ destruct (); } отключено ($ item); }
Джош

Ответы:

48

В PHP нет сборщика мусора. Он использует подсчет ссылок для управления памятью. Таким образом, наиболее частым источником утечек памяти являются циклические ссылки и глобальные переменные. Боюсь, что если вы используете фреймворк, вам придется перебирать много кода, чтобы найти его. Самый простой инструмент - это выборочно размещать вызовы memory_get_usageи сужать их до утечек кода. Вы также можете использовать xdebug для создания трассировки кода. Запустите код со следами выполнения и show_mem_delta.

Troelskn
источник
3
Но будьте осторожны ... сгенерированные файлы трассировки будут ОГРОМНЫМИ. В первый раз, когда я запустил трассировку xdebug в приложении Zend Framework, мне потребовалось очень много времени для запуска и создания файла размером в несколько ГБ (не КБ или МБ ... ГБ). Просто имейте это в виду.
rg88
1
Да, это довольно тяжело ... GB звучит немного много - если только у вас нет большого скрипта. Может быть, попробуйте просто обработать пару строк (этого должно хватить для выявления утечки). Кроме того, не устанавливайте расширение xdebug на рабочий сервер.
troelskn
31
Начиная с версии 5.3 в PHP есть сборщик мусора. С другой стороны, функция профилирования памяти удалена из xdebug :(
wdev
3
+1 нашел утечку! Класс с циклическими ссылками! Как только эти ссылки были отключены (), объекты были собраны мусором, как и ожидалось! Спасибо! :)
rinogo 06
@rinogo, так как ты узнал об утечке? Расскажите, какие шаги вы предприняли?
JohnnyQ 07
11

Вот трюк, который мы использовали, чтобы определить, какие скрипты используют больше всего памяти на нашем сервере.

Сохраните следующий фрагмент в файле, например, по адресу /usr/local/lib/php/strangecode_log_memory_usage.inc.php:

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

Используйте его, добавив в httpd.conf следующее:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

Затем проанализируйте файл журнала на /var/log/httpd/php_memory_log

Это может потребоваться, touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logпрежде чем ваш веб-пользователь сможет записывать в файл журнала.

Куинн Комендант
источник
8

Я заметил однажды в старом скрипте, что PHP будет поддерживать переменную «as» как в области видимости даже после моего цикла foreach. Например,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

Я не уверен, исправят ли это будущие версии PHP или нет, с тех пор, как я это видел. Если это так, вы можете unset($user)после doSomething()строки очистить ее из памяти. YMMV.

Patcoll
источник
13
PHP не поддерживает циклы / условия, такие как C / Java / и т. Д. Все, что объявлено внутри цикла / условия, остается в области видимости даже после выхода из цикла / условия (по замыслу [?]). С другой стороны, методы / функции имеют такую ​​же область видимости, как и следовало ожидать - все освобождается после завершения выполнения функции.
Фрэнк Фармер
Я предположил, что это сделано намеренно. Одним из преимуществ этого является то, что после цикла вы можете работать с последним найденным элементом, например, который удовлетворяет определенным критериям.
Иоахим
Вы могли бы unset()это сделать, но имейте в виду, что для объектов все, что вы делаете, это меняете место, на которое указывает ваша переменная - вы фактически не удалили ее из памяти. PHP в любом случае автоматически освободит память, как только она выйдет за рамки, поэтому лучшим решением (с точки зрения этого ответа, а не вопроса OP) является использование коротких функций, чтобы они не зависали от этой переменной из цикла. длинный.
Rich Court
@patcoll Это не имеет ничего общего с утечками памяти. Это просто изменение указателя массива. Взгляните здесь: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html на версию 3a.
Харм Смитс
7

Есть несколько возможных точек утечки памяти в php:

  • сам php
  • расширение php
  • библиотека php, которую вы используете
  • ваш PHP код

Достаточно сложно найти и исправить первые 3 без глубокого реверс-инжиниринга или знания исходного кода php. Для последнего вы можете использовать двоичный поиск кода утечки памяти с помощью memory_get_usage

Kingoleg
источник
91
Ваш ответ примерно такой же общий, какой он мог бы быть
TravisO
2
Жаль, что даже на php 7.2 они не могут исправить утечки основной памяти php. В нем нельзя запускать длительные процессы.
Афтаб Навид
6

Недавно я столкнулся с этой проблемой в приложении, при, как я полагаю, аналогичных обстоятельствах. Скрипт, который запускается в PHP cli и проходит множество итераций. Мой сценарий зависит от нескольких базовых библиотек. Я подозреваю, что причиной является конкретная библиотека, и я потратил несколько часов напрасно, пытаясь добавить соответствующие методы уничтожения в ее классы, но безрезультатно. Столкнувшись с длительным процессом преобразования в другую библиотеку (которая может иметь те же проблемы), я придумал грубый способ решения проблемы в моем случае.

В моей ситуации на linux cli я перебирал кучу пользовательских записей и для каждой из них создавал новый экземпляр нескольких созданных мною классов. Я решил попробовать создать новые экземпляры классов с помощью метода PHP exec, чтобы этот процесс выполнялся в «новом потоке». Вот действительно базовый пример того, о чем я говорю:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

Очевидно, что у этого подхода есть ограничения, и нужно знать об опасностях этого, так как было бы легко создать кроликовую работу, однако в некоторых редких случаях это может помочь преодолеть трудное положение, пока не будет найдено лучшее решение. , как и в моем случае.

Нейт Флинк
источник
6

Я столкнулся с той же проблемой, и моим решением было заменить foreach на обычный for. Я не уверен в специфике, но похоже, что foreach создает копию (или каким-то образом новую ссылку) на объект. Используя обычный цикл for, вы получаете прямой доступ к элементу.

Гуннар Лиум
источник
5

Я бы посоветовал вам проверить руководство по php или добавить gc_enable()функцию для сбора мусора ... То есть утечки памяти не влияют на работу вашего кода.

PS: php имеет сборщик мусора, gc_enable()который не принимает аргументов.

Kosgei
источник
3

Недавно я заметил, что лямбда-функции PHP 5.3 оставляют дополнительную память, используемую при их удалении.

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

Я не уверен, почему, но кажется, что каждая лямбда требует дополнительных 250 байт даже после удаления функции.

Xeoncross
источник
2
Я собирался сказать то же самое. Это было исправлено в 5.3.10 ( # 60139 )
Кристофер Айвс,
@KristopherIves, спасибо за обновление! Вы правы, это больше не проблема, поэтому я не должен бояться использовать их как сумасшедший.
Xeoncross
2

Если то, что вы говорите о том, что PHP выполняет сборку мусора только после того, как функция является истинной, вы можете обернуть содержимое цикла внутри функции в качестве обходного пути / эксперимента.

Барт ван Хёкелом
источник
1
@DavidKullmann На самом деле я думаю, что мой ответ неправильный. В конце концов, run()вызываемое - это тоже функция, в конце которой должен произойти сборщик мусора.
Барт ван Хеукелом
2

Одна огромная проблема, с которой я столкнулся, заключалась в использовании create_function . Как и в лямбда-функциях, он оставляет сгенерированное временное имя в памяти.

Другая причина утечки памяти (в случае Zend Framework) - это Zend_Db_Profiler. Убедитесь, что этот параметр отключен, если вы запускаете скрипты в Zend Framework. Например, в моем application.ini было следующее:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

Выполнение примерно 25000 запросов + загрузка перед этим довели объем памяти до 128 МБ (мой максимальный предел памяти).

Просто установив:

resources.db.profiler.enabled    = false

Достаточно было не больше 20 Мб

И этот скрипт выполнялся в CLI, но он создавал экземпляр Zend_Application и запускал Bootstrap, поэтому он использовал конфигурацию «разработки».

Очень помог запустить скрипт с профилированием xDebug

Энди
источник
2

Я не видел, чтобы это явно упоминалось, но xdebug отлично справляется с профилированием времени и памяти ( начиная с версии 2.6 ). Вы можете взять информацию, которую он генерирует, и передать ее пользовательскому интерфейсу по вашему выбору: webgrind (только время), kcachegrind , qcachegrind или другие, и он генерирует очень полезные деревья вызовов и графики, чтобы вы могли найти источники ваших различных проблем. .

Пример (qcachegrind): введите описание изображения здесь

Шон Дауни
источник
1

Я немного опоздал к этому разговору, но поделюсь кое-чем, что имеет отношение к Zend Framework.

У меня возникла проблема с утечкой памяти после установки php 5.3.8 (с использованием phpfarm) для работы с приложением ZF, которое было разработано с помощью php 5.2.9. Я обнаружил, что утечка памяти была вызвана в файле Apache httpd.conf, в моем определении виртуального хоста, где говорится SetEnv APPLICATION_ENV "development". После комментирования этой строки утечка памяти прекратилась. Я пытаюсь найти встроенный обходной путь в моем php-скрипте (в основном, определяя его вручную в основном файле index.php).

fronzee
источник
1
Вопрос говорит, что он работает в CLI. Это означает, что Apache вообще не участвует в процессе.
Максим
1
@Maxime Хороший вопрос, я не понял этого, спасибо. Ну что ж, надеюсь, какой-нибудь случайный гуглер получит пользу от записки, которую я оставил здесь, так как эта страница появилась у меня, когда я пытался решить мою проблему.
fronzee
Проверьте мой ответ на этот вопрос, может быть, это тоже был ваш случай.
Энди
Ваше приложение должно иметь разные конфигурации в зависимости от среды. В "development"среде обычно есть множество журналов и профилей, которых может не быть в других средах. Комментирование строки заставило ваше приложение использовать вместо этого среду по умолчанию, которой обычно является "production"или "prod". Утечка памяти все еще существует; код, который его содержит, просто не вызывается в этой среде.
Марко Рой
0

Я не видел, чтобы это упоминалось здесь, но одна вещь, которая может быть полезна, - это использование xdebug и xdebug_debug_zval ('variableName'), чтобы увидеть refcount.

Я также могу привести пример того, как расширение php мешает: Z-Ray Zend Server. Если сбор данных включен, использование памяти будет увеличиваться на каждой итерации, как если бы сборка мусора была отключена.

HappyDude
источник