Діагностування витоків пам’яті - Вичерпано дозволений об’єм пам’яті в # байт


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 або налагодження вуду можуть допомогти мені знайти та виправити проблему?


PS - Я нещодавно зіткнувся з проблемою саме цього типу речей. На жаль, я також виявив, що php має проблему знищення дочірнього об'єкта. Якщо ви зняли батьківський об'єкт, його дочірні об'єкти не звільняються. Потрібно переконатися, що я використовую модифікований unset, який включає рекурсивний виклик усіх дочірніх об'єктів __destruct тощо. Деталі тут: paul-m-jones.com/archives/262 :: Я роблю щось на зразок: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ item -> __ destruct (); } unset ($ 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

@rinogo так як ти дізнався про витік? Чи можете ви поділитися, які кроки ви зробили?
JohnnyQ

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 буде підтримувати змінну "як", як за обсягом, навіть після мого циклу 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 / тощо. Все, що оголошено всередині циклу / умовного, все ще знаходиться в області дії навіть після виходу з циклу / умовного (за задумом [?]). З іншого боку, методи / функції масштабуються, як і слід було очікувати - все випускається після закінчення виконання функції.
Frank Farmer

Я припустив, що це за задумом. Одна його перевага полягає в тому, що після циклу ви можете працювати з останнім знайденим елементом, наприклад, який відповідає певним критеріям.
Йоахім

Ви могли б 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. У ньому не можна запускати тривалі процеси.
Aftab Naveed

6

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

У моїй ситуації на Linux cli я перебирав купу записів користувачів і для кожного з них створював новий екземпляр декількох створених мною класів. Я вирішив спробувати створити нові екземпляри класів за допомогою методу exec PHP, щоб цей процес працював у "новому потоці". Ось справді основний зразок того, про що я маю на увазі:

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 на звичайний для. Я не впевнений у особливостях, але, схоже, 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 робить GC лише після функції, відповідає дійсності, ви можете обернути вміст циклу всередині функції як обхідний шлях / експеримент.


1
@DavidKullmann Насправді я думаю, що моя відповідь неправильна. Зрештою, те, run()що називається, - це також функція, в кінці якої має відбутися GC.
Барт ван Хейкелом,

2

Однією з величезних проблем у мене було використання create_function . Як і в лямбда-функціях, він залишає генеровану тимчасову назву в пам'яті.

Ще однією причиною витоків пам'яті (у випадку Zend Framework) є Zend_Db_Profiler. Переконайтеся, що це вимкнено, якщо ви запускаєте сценарії під Zend Framework. Наприклад, я мав у своєму застосунку.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, тому використовував конфігурацію "розвитку".

Це дійсно допомогло запустити сценарій із профілюванням xDebug


2

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

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


1

Я трохи запізнився з цією розмовою, але поділюся чимось, що стосується Zend Framework.

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


1
Питання говорить, що він працює в CLI. Це означає, що Apache взагалі не бере участі в процесі.
Максим

1
@Maxime Хороший момент, мені не вдалося це зрозуміти, дякую. Ну добре, сподіваюся, якийсь випадковий працівник Google виграє від замітки, яку я все-таки залишив тут, оскільки ця сторінка з’явилася для мене під час спроби вирішити мою проблему.
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.