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


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.