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


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.


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.


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


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()который не принимает аргументов.


3

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

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

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


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).


1
Вопрос говорит, что он работает в CLI. Это означает, что Apache вообще не участвует в процессе.
— Максим

1
@Maxime Хороший вопрос, я не понял этого, спасибо. Ну что ж, надеюсь, какой-нибудь случайный гуглер получит пользу от записки, которую я оставил здесь, так как эта страница появилась у меня, когда я пытался решить мою проблему.
— fronzee

Проверьте мой ответ на этот вопрос, может быть, это тоже был ваш случай.
— Энди

Ваше приложение должно иметь разные конфигурации в зависимости от среды. В "development"среде обычно есть множество журналов и профилей, которых может не быть в других средах. Комментирование строки заставило ваше приложение использовать вместо этого среду по умолчанию, которой обычно является "production"или "prod". Утечка памяти все еще существует; код, который его содержит, просто не вызывается в этой среде.
— Марко Рой

0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.