PHP Memory Leak when Throwing/Catching and Recursing

I spent the better part of the day chasing down this bug, so maybe Google in its infinite wisdom will help speed up your search for the same bug. It’s really an obvious and simple solution, but all the tough problems are like that, aren’t they?

The Situation

I am retrieving and manipulating binary trees of objects. Typically I can handle objects 9 levels deep without bumping into the PHP memory limit on our server (128MB), but occasionally an exceptionally large object will come up and my script will crash. To handle this, I set up some try/catch blocks. They notice when there are just 16MB left and throw an Almost out of memory error. That error is caught and the request is made again at 1/2 the original depth down to two levels deep (so really 9, 4, 2).You can see how I do this, with memory_get_usage(), in the code snippet below.

The Throw

 // Assumes memory limit is in M (eg. 128M)
 // 1048576 bytes per MB
 if((str_replace('M','',ini_get('memory_limit'))*1048576) - memory_get_usage() < 1048576*16){ // 16 MB headroom
    unset( -- all my vars go here to get unset --);
    throw new Exception("Almost out of memory");
 }

ini_get returns whatever is in your php.ini file (or possibly your .htaccess file). I turn that into bytes since that’s what memory_get_usage returns, and then verifies that it’s less than my decided max headroom.

The Catch : Broken

function getLevels($start_idx,$levels = 9){
   try {
            return $this->getBinaryTree($levels);
        } catch (Exception $e) {
            switch($e->getMessage()){
            case "Almost out of memory":
               $max_tree_depth = (int)($levels/2);
               if($max_tree_depth >= 2){
                    return $this->getLevels($start_idx,$max_tree_depth);
               }else {
                    throw new Exception("Out of memory even after getting tree with 2 levels");
               }
               break;
            default:
               throw $e;
         }
     }
}

The Problem

Since the Throw block was unsetting all the variables before actually throwing the error, I expected that the only overhead for this process would be the typical overhead associated with recursive calls. A whole bunch of debug and memory_get_usage() statements later I confirmed that this was not the case, and that my program was still crashing with the Out of memory even after getting tree with 2 levels error.

The Solution

After many fruitless paths, Google led me to this thread in a C/C++ forum where I read the phrase

an exception object is being created and

AH HA! An Exception OBJECT! I always knew that exceptions were objects. I knew that you could call methods on them, etc. but somehow it never occurred to me that they would take up memory and need to be freed up like any other object! All I had to do was unset the caught object before recursing and all was well once again.

The Catch : Fixed

function getLevels($start_idx,$levels = 9){
   try {
            return $this->getBinaryTree($levels);
        } catch (Exception $e) {
            switch($e->getMessage()){
            case "Almost out of memory":
               $max_tree_depth = (int)($levels/2);
               if($max_tree_depth >= 2){
                    unset $e; // <------------------------ RIGHT HERE!
                    return $this->getLevels($start_idx,$max_tree_depth);
               }else{
                    throw new Exception("Out of memory even after getting tree with 2 levels");
               }
               break;
            default:
               throw $e;
         }
     }
}

Summary

It was a shameful noobie mistake that I’m happy to share. In summary, Exceptions are objects, and if you don’t unset them when using recursion in your try/catch blocks, you might think you have a memory leak.

Best of luck!

This entry was posted in Programming, Projects and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *