100

I've encountered the dreaded error-message, possibly through-painstaking effort, PHP has run out of memory:

Allowed memory size of #### bytes exhausted (tried to allocate #### bytes) in file.php on line 123

Increasing the limit

If you know what you're doing and want to increase the limit see memory_limit:

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

Beware! You may only be solving the symptom and not the problem!

Diagnosing the leak:

The error message points to a line withing a loop that I believe to be leaking, or needlessly-accumulating, memory. I've printed memory_get_usage() statements at the end of each iteration and can see the number slowly grow until it reaches the limit:

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
}

For the purposes of this question let's assume the worst spaghetti code imaginable is hiding in global-scope somewhere in $user or Task.

What tools, PHP tricks, or debugging voodoo can help me find and fix the problem?

Mike B
  • 30,567
  • 12
  • 81
  • 109
  • P.S. -- I've recently run into a problem with this exact type of thing. Unfortunately, I also found that php has a child object destruction problem. If you unset a parent object, its child objects aren't freed. Having to make sure I use a modified unset that includes a recursive call to all child objects __destruct and so on. Details here: http://paul-m-jones.com/archives/262 :: I'm doing something like: function super_unset( $item ){ if( is_object( $item ) && method_exists( $item, "__destruct" ) ){ $item->__destruct(); } unset( $item ); } – Josh Apr 01 '10 at 21:56

13 Answers13

48

PHP doesn't have a garbage collector. It uses reference counting to manage memory. Thus, the most common source of memory leaks are cyclic references and global variables. If you use a framework, you'll have a lot of code to trawl through to find it, I'm afraid. The simplest instrument is to selectively place calls to memory_get_usage and narrow it down to where the code leaks. You can also use xdebug to create a trace of the code. Run the code with execution traces and show_mem_delta.

troelskn
  • 107,146
  • 23
  • 127
  • 148
  • 3
    But watch out... the generated trace files will be ENORMOUS. The first time I ran an xdebug trace on a Zend Framework app it took a loooong time to run and generated a multi GB (not kb or MB... GB) sized file. Just be aware of this. – rg88 May 11 '09 at 21:07
  • 1
    Yeah, it's pretty heavy .. GB's sounds a bit much though - unless you had a large script. Maybe try to just process a couple of rows (Should be enough to identify the leak). Also, don't install the xdebug extension on the production server. – troelskn May 11 '09 at 22:16
  • 31
    Since 5.3 PHP actually has an garbage collector. On the other hand, the memory profiling function has been removed fro xdebug :( – wdev Oct 12 '11 at 10:41
  • 3
    +1 found the leak! A class that had cyclic references! Once these references were unset(), the objects were garbage collected as expected! Thanks! :) – rinogo Sep 06 '13 at 01:33
  • @rinogo so how did you find out about the leak? Can you share what steps you took? – JohnnyQ Dec 07 '16 at 17:33
  • @JohnnyQ - It's been a while (as you can see from the timestamps), so I don't really recall... However, I think I monitored the memory usage of the code to verify that the leak was occurring. Then, I'd make a change (such as adding the `unset()`) and test. Once I found something that had an affect on the memory leaks, I knew I was making progress! Hopefully that helps! :) – rinogo Dec 08 '16 at 01:52
  • @rinogo makes sense. Thanks for getting back to me. – JohnnyQ Dec 08 '16 at 13:45
11

Here's a trick we've used to identify which scripts are using the most memory on our server.

Save the following snippet in a file at, e.g., /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');

Employ it by adding the following to httpd.conf:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

Then analyze the log file at /var/log/httpd/php_memory_log

You might need to touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_log before your web user can write to the log file.

Quinn Comendant
  • 5,880
  • 2
  • 27
  • 29
8

I noticed one time in an old script that PHP would maintain the "as" variable as in scope even after my foreach loop. For example,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

I'm not sure if future PHP versions fixed this or not since I've seen it. If this is the case, you could unset($user) after the doSomething() line to clear it from memory. YMMV.

patcoll
  • 936
  • 6
  • 8
  • 13
    PHP doesn't scope loops/conditionals like C/Java/etc. Anything declared inside a loop/conditional is still in scope even after exiting the loop/conditional (by design[?]). Methods/functions, on the other hand, are scoped as you would expect -- everything's released once function execution ends. – Frank Farmer Apr 22 '11 at 16:44
  • I've assumed that's by design. One benefit of it is that after a loop you can work with the last item you found, eg that satisfies particular criteria. – joachim Aug 18 '11 at 07:40
  • You could `unset()` it, but bear in mind that for objects, all you're doing is changing where your variable is pointing to - you haven't actually removed it from memory. PHP will automatically free up the memory once it's out of scope anyway, so the better solution (in terms of this answer, not the OP's question) is to use short functions so they're not hanging on to that variable from the loop for too long. – Rich Court Oct 16 '18 at 15:13
  • @patcoll This has nothing to do with memory leaks. This is simply the array pointer changing. Take a look here: https://www.prismnet.com/~mcmahon/Notes/arrays_and_pointers.html at version 3a. – Harm Smits Mar 04 '19 at 08:32
7

There are several possible points of memory leaking in php:

  • php itself
  • php extension
  • php library you use
  • your php code

It is quite hard to find and fix the first 3 without deep reverse engineering or php source code knowledge. For the last one you can use binary search for memory leaking code with memory_get_usage

kingoleg
  • 1,136
  • 12
  • 23
6

I recently ran into this problem on an application, under what I gather to be similar circumstances. A script that runs in PHP's cli that loops over many iterations. My script depends on several underlying libraries. I suspect a particular library is the cause and I spent several hours in vain trying to add appropriate destruct methods to it's classes to no avail. Faced with a lengthy conversion process to a different library (which could turn out to have the same problems) I came up with a crude work around for the problem in my case.

In my situation, on a linux cli, I was looping over a bunch of user records and for each one of them creating a new instance of several classes I created. I decided to try creating the new instances of the classes using PHP's exec method so that those process would run in a "new thread". Here is a really basic sample of what I am referring to:

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
}

Obviously this approach has limitations, and one needs to be aware of the dangers of this, as it would be easy to create a rabbit job, however in some rare cases it might help get over a tough spot, until a better fix could be found, as in my case.

Nate Flink
  • 3,764
  • 2
  • 27
  • 18
6

I came across the same problem, and my solution was to replace foreach with a regular for. I'm not sure about the specifics, but it seems like foreach creates a copy (or somehow a new reference) to the object. Using a regular for loop, you access the item directly.

Gunnar Lium
  • 5,683
  • 9
  • 33
  • 33
5

I would suggest you check the php manual or add the gc_enable() function to collect the garbage... That is the memory leaks dont affect how your code runs.

PS: php has a garbage collector gc_enable() that takes no arguments.

jackslash
  • 8,515
  • 42
  • 56
Kosgei
  • 51
  • 1
  • 1
3

I recently noticed that PHP 5.3 lambda functions leave extra memory used when they are removed.

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

I'm not sure why, but it seems to take an extra 250 bytes each lambda even after the function is removed.

Xeoncross
  • 50,836
  • 73
  • 238
  • 351
  • 2
    I was going to say the same. This has been fixed as of 5.3.10 ([#60139](http://bugs.php.net/60139)) – Kristopher Ives Feb 28 '12 at 17:51
  • @KristopherIves, thanks for the update! You're right, this is no longer a problem so I shouldn't be afraid of using them like crazy now. – Xeoncross Feb 28 '12 at 18:22
2

One huge problem I had was by using create_function. Like in lambda functions, it leaves the generated temporary name in memory.

Another cause of memory leaks (in case of Zend Framework) is the Zend_Db_Profiler. Make sure that is disabled if you run scripts under Zend Framework. For example I had in my application.ini the folowing:

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

Running approximately 25.000 queries + loads of processing before that, brought the memory to a nice 128Mb (My max memory limit).

By just setting:

resources.db.profiler.enabled    = false

it was enough to keep it under 20 Mb

And this script was running in CLI, but it was instantiating the Zend_Application and running the Bootstrap, so it used the "development" config.

It really helped running the script with xDebug profiling

Andy
  • 6,419
  • 2
  • 25
  • 22
2

I didn't see it explicitly mentioned, but xdebug does a great job profiling time and memory (as of 2.6). You can take the information it generates and pass it off to a gui front end of your choice: webgrind (time only), kcachegrind, qcachegrind or others and it generates very useful call trees and graphs to let you find the sources of your various woes.

Example (of qcachegrind): enter image description here

SeanDowney
  • 16,172
  • 17
  • 77
  • 88
2

If what you say about PHP only doing GC after a function is true, you could wrap the loop's contents inside a function as a workaround/experiment.

Bart van Heukelom
  • 40,403
  • 57
  • 174
  • 291
  • 1
    @DavidKullmann Actually I think my answer is wrong. After all, the `run()` that is called is also a function, at the end of which the GC should happen. – Bart van Heukelom Feb 18 '12 at 16:49
1

I'm a little late to this conversation but I'll share something pertinent to Zend Framework.

I had a memory leak problem after installing php 5.3.8 (using phpfarm) to work with a ZF app that was developed with php 5.2.9. I discovered that the memory leak was being triggered in Apache's httpd.conf file, in my virtual host definition, where it says SetEnv APPLICATION_ENV "development". After commenting this line out, the memory leaks stopped. I'm trying to come up with an inline workaround in my php script (mainly by defining it manually in the main index.php file).

fronzee
  • 1,396
  • 2
  • 17
  • 30
  • 1
    The question says he is running in CLI. That means Apache is not involved at all in the process. – Maxime Mar 16 '12 at 16:31
  • 1
    @Maxime Good point, I failed to catch that, thanks. Oh well, hopefully some random Googler will benefit from the note I left here anyway, since this page came up for me while trying to solve my problem. – fronzee Mar 16 '12 at 20:17
  • Check my answer on this question, maybe that was your case too. – Andy Jun 22 '12 at 14:08
  • Your application should have different configurations depending on the environment. The `"development"` environment usually has a bunch of logging & profiling that other environments might not have. Commenting the line out just made your application use the default environment instead, which is usually `"production"` or `"prod"`. The memory leak still exists; the code that contains it is just not being called in that environment. – Marco Roy Sep 17 '15 at 17:52
0

I didn't see it mentioned here but one thing that might be helpful is using xdebug and xdebug_debug_zval('variableName') to see the refcount.

I can also provide an example of a php extension getting in the way: Zend Server's Z-Ray. If data collection is enabled it memory use will balloon on each iteration just as if garbage collection was off.

HappyDude
  • 302
  • 3
  • 11