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

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

Разрешенный размер памяти #### байт исчерпан (попытался выделить #### bytes) в файле .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 могут помочь мне найти и устранить проблему?

Ответ 1

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

Ответ 2

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

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

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

Ответ 3

Я заметил один раз в старом script, что 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.

Ответ 4

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

В моей ситуации, в 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
}

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

Ответ 5

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

Ответ 6

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

Сохраните следующий фрагмент файла в файле, например, /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, прежде чем ваш веб-пользователь сможет записать в файл журнала.

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

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

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

Запустив примерно 25 000 запросов + загрузок до этого, доведя память до хорошего 128 Мб (максимальный предел максимальной памяти).

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

resources.db.profiler.enabled    = false

этого было достаточно, чтобы сохранить его ниже 20 МБ

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

Это действительно помогло запустить script с профилем xDebug

Ответ 11

Я немного опаздываю на этот разговор, но я расскажу кое-что, относящееся к Zend Framework.

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

Ответ 12

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

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