Как найти утечку памяти php

My dedicated server has 32GB RAM and the memory just goes up and up constantly and I have to reboot it daily now. This is costing me customers and money.

I am having a hard time finding where the memory leak is. All I can find online is people say «Use xdebug» but I haven’t been able to find any xdebug tutorials on finding memory leaks. I have tried printing memory_get_usage before and after function calls but is that the right way to do it?

I have MANY php scripts running — some from visitors, others from cron jobs — and I need to find which one(s) of them is leaking memory and fix it ASAP but I don’t even know how to determine if a given function is leaking memory or not.

I have tried printing memory_get_usage before a function call and after, and it goes up, but then if I call the function more than once, it doesn’t go up anymore. Can someone please explain this and tell me how I can simply and easily tell if a PHP function has a memory leak?

asked Apr 17, 2013 at 19:15

Guy's user avatar

4

You could do various things, but first you should try to avoid the creation of memory leaks in the first place.

Let me clarify: PHP is a scripting language and it is not designed for long running scripts, so it’s memory management is not the best on the market. But why should it be? It’s purpose is to be called on a request level so its running scope is quite small (not more than 2 — 3 seconds). Everything else should be put in the background.

What can I do against memory leaks?

  1. If you are at a version below 5.4 you need to take care of circle references, since those are not garbage collected.

  2. If you need a script to be run continuously, you might think about a different approach. Do try a while(true) implementation, but wrap supervisor (http://supervisord.org) around your script, and let it be called after it ends. That way you make 100% sure you never get memory leaks.

  3. You could use xdebug to profile your scripts one by one and find out, where a lot of memory is consumed.

  4. You could implement a destructor to unset all you references if the class is not of any need anymore.

    public function __destruct(){
        $this->cleanup();
    }
    
    public function cleanup() {
        //cleanup everything from attributes
        foreach (get_class_vars(__CLASS__) as $clsVar => $_) {
            unset($this->$clsVar);
        }
    
        //cleanup all objects inside data array
        if (is_array($this->_data)) {
            foreach ($this->_data as $value) {
                if (is_object($value) && method_exists($value, 'cleanUp')) {
                    $value->cleanUp();
                }
            }
        }
    }
    
  5. Read through the PHP documentation regarding garbage collection http://us3.php.net/manual/en/features.gc.php

  6. Avoid global variables, because those are never garbage collected and need to be unset explicitly. If you are using a Framework like ZF or Symfony that might not be possible, since you would break functionality if you do.

Last but not least I want to emphasize once again, PHP is not suited for long running scripts! If you have things to do, that need to run continuously you should not crumble your head with memory leaks in PHP, but take the time to learn a more sophisticated language like JAVA or C#.

answered Apr 26, 2013 at 17:26

MatthiasLaug's user avatar

MatthiasLaugMatthiasLaug

2,9146 gold badges27 silver badges43 bronze badges

3

I found method which works pretty well for me:

  1. Install «php-memprof» extention. In you can Ubuntu run:

    sudo pecl install memprof

  2. Install «google-perftools». Again for Ubuntu:

    sudo apt-get install google-perftools

  3. Add this code to begining of your script:

    if (function_exists('memprof_enable')) {
        memprof_enable();
    }
    
  4. And this aroud place were you expexct to find memory leak:

    if (function_exists("memprof_dump_pprof"))
    {
        $time = microtime(true);
        $f = fopen("/tmp/profile_$time.heap", "w");
        memprof_dump_pprof($f);
        fclose($f);
        echo "Memory profile dumped. ";
    }
    

    In my case it was inside big cycle every 100 runs.

  5. Run google-pprof comparing 2 memory dumps:

    google-pprof --web --base=/tmp/profile_17.heap /tmp/profile_18.heap
    

    This will open svg image like this in your browser:

    sample from doc

    Description of numbers and names inside you can find in gperftools documentation

P.S. Fixing leaks on php-level will not guarantee you that there are no memory-leaks in interpreter. In my case I end up just with restarting sctipt in longer periods.

Community's user avatar

answered Mar 29, 2016 at 15:30

red_led's user avatar

red_ledred_led

3434 silver badges7 bronze badges

0

I’m not an expert on memory usage, but maybe this method will help you detect the problematic scripts:

Get information:
1. Use the apache access log files
2. Create your own memory usage log file (http://www.webhostingtalk.com/showthread.php?t=617742)

Check the time when the memory usage goes up and compare to the apache access log.

It will at least give you information whether the usage goes up slowly and constant or if it starts at a certain point.

Good luck!

answered May 3, 2013 at 14:33

Hashbrown's user avatar

Memory leaks can happen in any language, including PHP. These memory leaks may happen in small increments that take time to accumulate, or in larger jumps that manifest quickly. Either way, if your app has a memory leak, sooner or later it will cause problems. The source of and solution to PHP memory leaks aren’t always obvious, so you may need to try a few strategies before you eliminate the problem.

PHP Memory Usage: How Memory Leaks Happen 

What is a memory leak in PHP?

A memory leak in PHP is a condition that causes sections of code to continue using memory even though that memory is no longer needed. There are several ways for memory leaks to occur. Variables that never go out of scope, cyclical references, extensions in C that `malloc` instead of `emalloc` and for whatever reason don’t `free`, to name a few. There are surprising and quite subtle ways of using and holding on to memory in PHP. If enough memory is leaking, your application will eventually run into memory limits imposed by PHP settings or by the OS itself, and crash.

Does PHP have garbage collection?

Yes, PHP does have garbage collection that should help prevent memory leaks. However, several factors can prevent the garbage collector from fulfilling its task. For example, if an object’s refcount increases and never decreases, then the object is still technically in use and is not garbage. The garbage collector, therefore, cannot identify it as such and will not free up the object. 

Garbage collection is active in PHP by default, but it can be disabled through settings in `php.ini`. If garbage collection is disabled, you’ll quickly accumulate unused memory that’s never freed. Disabling garbage collection can improve performance for scripts that are short-lived and completely exit (and therefore free all used memory), but for longer scripts and daemons you’ll probably want the garbage collector enabled.

How to Find PHP Memory Leaks

Identifying the source of your PHP memory leak is the first step to finding a solution. You can’t fix a problem until you understand its underlying cause.

Option One: Log Your Scripts

If you’re running multiple scripts, then you need to determine which one is causing the leak. Use `auto_append_file` and `memory_get*` to generate a log of your scripts. Then, review the scripts to see which ones use the most memory.

Option Two: Discover Peak Usage

Use `memory_get_peak_usage` to find out how much memory has been allocated to your script. If it looks abnormally high, then you might first look at your PHP script for code that may be unintentionally loading or iterating over more data than anticipated. If so, break down the loading or processing of the data into manageable chunks instead of all-at-once.

Option Three: Use a PHP Memory Profiler

Use the php-memprof extension to learn how much memory is still in use after a script runs.

Monitoring Memory Usage with a PHP Memory Profiler

Monitoring memory usage with a PHP memory profiler can make it much easier for you to spot problems within your scripts. Several PHP profilers include features that will detect memory leaks.

What is memory profiling?

Memory profiling scans your PHP scripts to see precisely how each function uses memory. The level of depth that you get will depend on the PHP memory profiler that you choose. Some, for example, will show you how much memory your functions use and continue use while running a script. Others will point you directly to problematic functions that cause PHP memory leaks and other performance issues.

Finding the right memory profiler is an essential part of debugging PHP scripts with memory leaks. 

Xhprof PHP Memory Profiler

Xhprof has a simple user interface that will help you discover PHP memory leaks. It can also identify the performance issues that make PHP memory leaks happen.

Xdebug PHP Profiler

Xdebug is a standard PHP profiler that you can use to discover a variety of performance issues in your scripts. The lightweight profiler doesn’t use much memory, so you can run it alongside your PHP scripts for real-time performance debugging.

PHP-memprof

PHP-memprof is a stand-alone PHP memory profiler that can tell you exactly how much memory each of your functions uses. It can even trace an allocated byte back to a function.

Dig in!

It’s inevitable that at some point you’ll have to track down a memory issue with your PHP app. Unfortunately, memory leaks are the type of bug that can be notoriously difficult to hunt and fix. With the options covered here, we hope you have a good starting point to uncover and resolve any potential memory problems you encounter with your PHP application!

In this article, I will cover my journey to find and fix a memory leak in a PHP
application. The final patch is simple, but only the journey is important, right?

Section intitulée introductionIntroduction

In our application, we had a worker that consumed a lot of RAM. After 10 seconds, the consumption reached about 1.5Gb! I use to find and eradicate memory
leak quite quickly, but this time, it caused me a lot of trouble.

In the past, I used php-meminfo, a very
good extension. But it is not
compatible with PHP 7.3+ yet.
Unfortunately, we run PHP 7.4.

So I use primitive tool: I added few calls to memory_get_usage() in my
worker. And… surprise it reported very low memory usage: about 50Mb whereas my
OS reported more than 1Gb. What the hell is going on here?

Then I tried blackfire, same, it’s not able to see what’s going on.

I needed to do my homework, so I re-read an old article written by Julien Pauli
about Zend Memory
Manager.

To summarize this article very quickly:

  • PHP adds a layer on top of your OS to manage the memory;
  • When you declare a variable, PHP uses the memory manager to allocate the RAM;
  • When you call memory_get_usage(), PHP asks the memory manager how much memory it has allocated.

OK, the issue should not be my code nor the vendor. It should be in an extension, or PHP itself ! But I may be wrong :)

Section intitulée what-is-in-the-memoryWhat is in the memory?

The application has too many lines of code, and since memory_get_usage()
reports the wrong memory usage, I’ll need to find another way to find this leak.

I decided to see what’s in the RAM to make decisions. To do that I started to
look at what part of RAM was growing. At this point I was pretty sure the issue was
in an extension. I ran the following command twice :

sudo cat /proc/<PID>/maps > before # or after
  • when the worker started, but before handling messages;
  • after 15s of handling messages.

And I made a diff on these two files.

Surprise, the HEAP grew a lot. Let’s dump it thanks to following command (I
found the memory addresses thanks to the previous command):

$ sudo gdb -p <PID>
dump memory ./memory.dump 0x1234567 0x98765432

Since it’s full of binary data, I used my favorite command in such situation:

strings memory.dump > memory.dump.string

And then I opened the file with vim. It was full of HTML. OK, I think I found the
culprit.

Section intitulée make-a-reproducerMake a reproducer

The worker was responsible of the following tasks :

  • Read data (HTML) from RabbitMQ with the AMQP extension;
  • Analyze theses data with the DOM extension;
  • Publish results in RabbitMQ.

So I make a reproducer to test each part of the code:

$count = 25_000;
​
// Blank
for ($i=0; $i < $count ; $i++) {
    $content = file_get_contents(__DIR__. "/fixtures/$i.txt");
}
​
// PCRE
for ($i=0; $i < $count ; $i++) {
    $content = file_get_contents(__DIR__. "/fixtures/$i.txt");
    preg_match('/title/', $content, $m);
}
​
// DOM
for ($i=0; $i < $count ; $i++) {
    $content = file_get_contents(__DIR__. "/fixtures/$i.txt");
    $d = new Crawler($content);
    $t = $d->filter("title");
}
​
// JSON
for ($i=0; $i < $count ; $i++) {
    $content = file_get_contents(__DIR__. "/fixtures/$i.txt");
    json_encode($content);
}
​
// AMQP
$channel = $c->get(Broker::class)->getAmqpChannel();
$exchange = new AMQPExchange($channel);
$exchange->setType('direct');
$exchange->setName('leak');
$exchange->declare();
$queue = new AMQPQueue($channel);
$queue->setName('leak');
$queue->setArgument('x-queue-mode', 'lazy');
$queue->declare();
$queue->bind('leak', 'leak');
for ($i=0; $i < $count ; $i++) {
    $content = file_get_contents(__DIR__. "/fixtures/$i.txt");
    $exchange->publish($content, 'leak', AMQP_NOPARAM, ['delivery_mode' => 2]);
}
for ($i=0; $i < $count ; $i++) {
    $envelope = $queue->get();
    if (!$envelope) {
        break;
    }
    $queue->ack($envelope->getDeliveryTag());
}

And I benched the code. Nothing was wrong here. Bad news! Or good news: PHP does not leak.

Section intitulée reconsider-everythingReconsider everything

So I go back to my code, and I started to bypass some part of the code, until the application does not leak.

I was in the part I thought in the beginning: the analysis of HTML. So now I’m able to create a new reproducer, with the exact part of what is going badly:

use MastermindsHTML5;

require __DIR__.'/vendor/autoload.php';

$html = file_get_contents('https://www.php.net/');
$html5 = new HTML5();
$dom = $html5->loadHTML($html);
echo "Converting to HTML 5n";
for ($i=0; $i < 100; $i++) {
    $html5->saveHTML($dom);  // This is this line in my application that leak
    printf("%.2fn", memory_get_usage(false) / 1024 / 1024);
}

The results were a bit crazy, the value kept growing .

The fix was pretty obvious and easy.

Section intitulée but-waitBut wait

At this point I was a bit confused: I managed to find a leak with memory_get_usage(), but I said the leak could not be found with this tool. Actually I found an additional leak.

So I started to dig again, and I managed to create this reproducer:

$content = file_get_contents('https://www.php.net/');

$count = $argv[1] ?? 251;

for ($i = 0; $i < $count; $i++) {
    $crawler = new Crawler($content);
    $nodes = $crawler->filterXPath('descendant-or-self::head/descendant-or-self::*/title');
    $nodes->each(static function ($node): void {
        $node->html();
    });
    if (0 == $i % 10) {
        preg_match('/^VmRSS:s(.*)/m', file_get_contents('/proc/self/status'), $m);
        printf("%03d - %.2fMb - %sn", $i, memory_get_usage(true) / 1024 / 1024, trim($m[1]));
    }
}

This code could be simplified, but it looks like what I have in the application.
As you can see, I used two methods to get the memory usage:

  • memory_get_usage(): This is what is seen by PHP and its memory manager;
  • /proc/self/status: This reports information seen by my OS. This is much more accurate than the former.

And here the result where astonishing:

i   - PHP    - OS       - Duration
000 - 4.00Mb - 37936 kB - 0.084s
010 - 4.00Mb - 45648 kB - 0.530s
020 - 4.00Mb - 53040 kB - 0.991s
030 - 4.00Mb - 60696 kB - 1.488s
040 - 4.00Mb - 68352 kB - 1.981s
050 - 4.00Mb - 76008 kB - 2.455s
060 - 4.00Mb - 83400 kB - 2.973s
070 - 4.00Mb - 91056 kB - 3.576s
080 - 4.00Mb - 98712 kB - 4.208s
090 - 4.00Mb - 106368 kB - 4.682s
100 - 4.00Mb - 113760 kB - 5.146s
110 - 4.00Mb - 121416 kB - 5.622s
120 - 4.00Mb - 129072 kB - 6.098s
130 - 4.00Mb - 136728 kB - 6.561s
140 - 4.00Mb - 144120 kB - 7.024s
150 - 4.00Mb - 151776 kB - 7.491s

The leak is terrible. In 150 iterations, it consumes more than 150Mb

PHP does not see any increase, but my OS does. How could it be?

Section intitulée what-is-the-real-causeWhat is the real cause?

I read a bit the code, and I saw that:

$rules = new OutputRules($stream, $options);
$trav = new Traverser($dom, $stream, $rules, $options);

and in the Traverser constructor:

$this->rules->setTraverser($this);

We have a cyclic reference here. And this is something PHP does not like. It
makes freeing memory harder. Only the Garbage Collector can solve this issue.

I could let the GC do its job, but this code was on a critical path, where we
need extreme performance. Moreover, the GC does not run every time. It is
triggered whenever 10000 possible cyclic objects or arrays are currently in
memory and one of them falls out of scope.

But 2 objects in memory, that should not be that bad? No it’s not until I saw that:

$this->dom = $dom;

OK! Here we have a demoniac combination:

  • We store some data in a DOMElement. This data is not managed by the memory manager. It’s in the libxml extension. That’s exactly why the Zend Memory Manager could not see the memory leak;
  • We have a cyclic reference. PHP could not clear this data quickly. Only the GC can.

Section intitulée conclusionConclusion

I eventually made another
patch to mitigate this leak.

In this patch, I “help” PHP to free memory by breaking the circular reference.
The Garbage Collector is not involved anymore, and the memory stays constant and
very low.

I’m happy.

So how to prevent such issue:

  • First, avoid as much as possible cyclic references. You may read everywhere
    that it often reflects a bad design (it’s not always the case);
  • When working with stream, be really careful or you may invoke some development esthete.

ilyaplot

Есть скрипт, который обрабатывает большое количество данных. Сделал unset переменных, которые больше не нужны на каждой итерации, но память продолжает утекать с бешеной скоростью. Есть ли способы определить источник утечки в коде?


  • Вопрос задан

    более трёх лет назад

  • 3921 просмотр

Какие библиотеки используются?
Могут давать утечку классы по ресайзу картинок, хмл парсеры итп.
Если юзается phpQuery в цикле, то
phpQuery::unloadDocuments()

Пригласить эксперта

Есть.
Причина утечки — прокладка. Между стулом и монитором.
В данном случае причина —

Получаю массив записей из бд

Получать записи надо не массивом, а по одной.
И никакая память никуда утекать не будет. ДАЖЕ если не ансетить те переменные, которые и так будут перезаписаны при следующей итерации.


  • Показать ещё
    Загружается…

27 мая 2023, в 10:30

2000 руб./за проект

27 мая 2023, в 10:26

10000 руб./за проект

27 мая 2023, в 10:16

700 руб./в час

Минуточку внимания

Hunting Down Memory Leaks

Why getting rid of Memory Leaks ?

The main consequences of a memory leak are:

  • increasing memory usage
  • decreasing performances

Decreasing performances is usually the most visible part. As memory leak
saturates the garbage collector buffer, it runs far more often, without
being able to free any memory.

This leads to a high CPU usage of the process, with a lot of time spent
in the garbage collector instead of your code (garbage collector doesn’t
run in parallel with the user code in PHP, so the garbage collector has to pause the user code).

See https://speakerdeck.com/bitone/hunting-down-memory-leaks-with-php-meminfo
for a more detailed insight on how memory is released in PHP and how memory
leaks can occur.

How to hunt Memory Leak Down

Process Overview

  1. Dump memory state.
  2. Display a summary of items in memory.
  3. Find an item that shouldn’t be in memory anymore, but is part of the dump.
  4. Track down the reference that are still holding the object in memory.
  5. Fix the code.
  6. Profit!

1. Dumping Memory State

From anywhere in your PHP code, the following line will dump the current state
of the PHP memory, including all the items alive in memory:

meminfo_dump(fopen('/tmp/php_mem_dump.json', 'w'));

2. Memory state overview with the summary analyzer

From the analyzer directory:

$ bin/analyzer summary /tmp/php_mem_dump.json
+----------+-----------------+-----------------------------+
| Type     | Instances Count | Cumulated Self Size (bytes) |
+----------+-----------------+-----------------------------+
| string   | 132             | 7079                        |
| MyClassA | 100             | 7200                        |
| array    | 10              | 720                         |
| integer  | 5               | 80                          |
| float    | 2               | 32                          |
| null     | 1               | 16                          |
+----------+-----------------+-----------------------------+

The summary gives you an aggregated view of items by type or class.
It’s the best tool to know where to look at, as most of the time a memory
leak will generate objects of the same class.

So a huge volume of instances of a particular class is a good indicator of the
direction to take to track down the memory leak.

Even better is to follow the evolution of the summary, so the leak will be even
more apparent as the number of items leaking will be the one increasing over time.

Once we have a good idea of the kind of items that are leaking, let’s go deeper
and try to find why there are still in memory.

3. Finding objects with the most children

$ bin/analyzer top-children /tmp/my_dump_file.json
+-----+----------------+----------+
| Num | Item ids       | Children |
+-----+----------------+----------+
| 1   | 0x7ffff4e22fe0 | 1000000  |
| 2   | 0x7fffe780e5c8 | 11606    |
| 3   | 0x7fffe9714ef0 | 11602    |
| 4   | 0x7fffeab63ca0 | 3605     |
| 5   | 0x7fffd3161400 | 2400     |
+-----+----------------+----------+

Perhaps some array accumulates logs or caches something. This command helps you to find items with many children.
But be careful this command does not summarize multi-dimensional arrays.
For resolve ID filed see the next paragraph — «Finding references to the leaked object».

4. Finding an instance of a leaked object

So now, we know of what class are the object leaking. It can be multiple classes in the case of
objects composed with multiple other objects.

But only one class is needed for now. Let’s take our MyClassA example.

We will use the analyzer’s query command to find a single instance of this class
that is still in memory, and that is not directly attached to a variable declared in a
living scope (is_root at 0).

$ bin/analyzer query -v -f "class=MyClassA" -f "is_root=0" /tmp/php_mem_dump.json
+----------------+-------------------+------------------------------+
| Item ids       | Item data         | Children                     |
+----------------+-------------------+------------------------------+
| 0x7f94a1877008 | Type: object      | myObjectName: 0x7f94a185cca0 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 1  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f94a1877028 | Type: object      | myObjectName: 0x7f94a185cde0 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 2  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f94a1877048 | Type: object      | myObjectName: 0x7f94a185cf20 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 3  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f94a1877068 | Type: object      | myObjectName: 0x7f94a185d060 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 4  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f94a1877088 | Type: object      | myObjectName: 0x7f94a185d1a0 |
|                | Class: MyClassA   |                              |
...

We have a list of ten items matching our query (use the -l option to get more).

So let’s take one of them, for example 0x7f94a1877068, and find out what are
the references that are still alive and pointing to it.

5. Finding references to the leaked object

We know this object is still in memory, and we know he is not linked directly
to a variable (not root). So it means another item (like an array or an object)
has reference to our object. Maybe our object is in an array or maybe it’s a member
of another object.
And maybe the container of our object is attached directly to a variable. It’s a root.

But maybe, it’s not. Maybe the container itself is part of an array or another object.
And it can goes very far until you find the object or array that is the root of that.

To automatically find the path from our leaked object to the root that makes it stay
in memory, we will use the ref-path command.

$ bin/analyzer -v ref-path 0x7f94a1877068 /tmp/php_mem_dump.json
Found 1 paths
Path from 0x7f94a1856260
+--------------------+
| Id: 0x7f94a1877068 |
| Type: object       |
| Class: MyClassA    |
| Object Handle: 4   |
| Size: 72 B         |
| Is root: No        |
| Children count: 1  |
+--------------------+
         ^
         |
         3
         |
         |
+---------------------+
| Id: 0x7f94a185cb60  |
| Type: array         |
| Size: 72 B          |
| Is root: No         |
| Children count: 100 |
+---------------------+
         ^
         |
    second level
         |
         |
+--------------------+
| Id: 0x7f94a185ca20 |
| Type: array        |
| Size: 72 B         |
| Is root: No        |
| Children count: 1  |
+--------------------+
         ^
         |
    first level
         |
         |
+---------------------------+
| Id: 0x7f94a1856260        |
| Type: array               |
| Size: 72 B                |
| Is root: Yes              |
| Execution Frame: <GLOBAL> |
| Symbol Name: myRootArray  |
| Children count: 1         |
+---------------------------+

So the reason why our object is still in memory is because it’s in an array,
that is itself in another array that is in an final array. And the final array
is directly linked to the variable declared in the <GLOBAL> frame and called myRootArray.

In a shorter PHP-ish notation, this could be written like that:

$myRootArray['first level']['second level'][3] = 0x7f94a1877068;

So now, we know we are leaking this object because we still have a reference
going down to this declared $myRootArray variable.

This tremendously helps understand the reason of the memory leak. Now that we
have the leaking objects and the item that keep reference to them, we have
all the information needed to debug the code.

It’s your turn to play ;)

Video presentation in English

A French version:
https://www.youtube.com/watch?v=wZjnj1PAJ78

Понравилась статья? Поделить с друзьями:
  • Как найти своего мастера ногтевого сервиса
  • Как найти грамматику в предложении
  • Как составить прайс лист художнику
  • Как найти плотность шара формула
  • Как найти синус 135 градусов формула