Диагностика утечек памяти - допустимый объем памяти исчерпан # байтами - PullRequest
92 голосов
/ 11 мая 2009

Я столкнулся с ужасным сообщением об ошибке, возможно, из-за кропотливых усилий, 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 могут помочь мне найти и исправить проблему?

Ответы [ 13 ]

45 голосов
/ 11 мая 2009

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

11 голосов
/ 11 мая 2009

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

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

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

9 голосов
/ 15 декабря 2013

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

Сохраните следующий фрагмент в файле, например, /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 голосов
/ 15 мая 2009

Однажды в старом скрипте я заметил, что 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.

6 голосов
/ 20 апреля 2011

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

6 голосов
/ 20 июля 2010

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

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

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

3 голосов
/ 07 декабря 2011

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

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

3 голосов
/ 12 июля 2011

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

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

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

2 голосов
/ 19 февраля 2018

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

Пример (из qcachegrind): enter image description here

2 голосов
/ 22 июня 2012

Одна огромная проблема, с которой я столкнулся при использовании 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 Мб

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

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

...