Hello All,

I've been struggling with a rather obscure PHP memory issue for the
last few days.  Here's my setup:

FreeBSD 5.5
Apache2.0, PHP 5.1.6, and MySQL 4.1.x compiled from scratch (that is,
we're not using FreeBSD ports)
PHP is compiled with --enable-memory-limit and the limit is set to 8MB

We're developing a web application that involves traversal of a
hierarchical database structure (MySQL, PEAR::DB, and
PEAR::DB::DataObject).  Currently that traversal is done recursively,
and involves visiting thousands of nodes in the tree.  However, the
tree is relatively flat, and the recursion never gets more than 4 or 5
calls deep.  A severely truncated but illustrative version of the code
of interest is:

<?php

// ... setup database connection and other infrastructure ...

trigger_error(memory_get_usage());
$result = traverse_hierarchy();
trigger_error(memory_get_usage());

// ... do something with $result

?>

Exactly what the traverse_hierarchy function does is mostly irrelevant
to my question, as will become clear in a moment.

In practice, the first trigger_error outputs ~2.5MB (mostly in
$GLOBALS['_DB_DATAOBJECT']...), and the 2nd, after the traversal,
outputs ~8MB.  This puts us dangerously near our limit, and frequently
causes the application to quit as it reaches the 8MB limit.

The problem is this: the amount of data gathered is nowhere near the
~5.5 MB that PHP claims has been allocated during execution of
traverse_hierarchy, and attempts to figure out WHERE that memory has
been assigned have failed.  Consider the following alterations to the
code:

<?php

// ... setup database connection and other infrastructure ...

trigger_error(memory_get_usage());
$result = traverse_hierarchy();
trigger_error(memory_get_usage());
clear_globals();
trigger_error(memory_get_usage());

// function to iteratively unset all globals
function clear_globals() {
        foreach ($GLOBALS as $k => &$v) {
                if ($k === 'GLOBALS')
                        continue;
                $before = memory_get_usage();
                unset($v);
                unset($$k);
                unset($GLOBALS[$k]);
                $after = memory_get_usage();
                trigger_error($k.':'.($before-$after));
                unset($k);
        }
        unset($GLOBALS);
}

?>

The above code outputs the memory allocated before and after the
traversal, and before and after a function call (clear_globals) that
iteratively 'unset's all members of the $GLOBALS array.  It also
outputs the amount of memory reclaimed after each unset of the members
of $GLOBALS.

When I wrote this I expected something like the following (ignoring
the superfluous error text generated by trigger_error):

2500000
8000000
(key):(memory released)
.
.
.
40000

Where 40000 is about the minimum memory footprint of an empty PHP
script as reported by memory_get_usage(), and where one of the
(key):(memory released) outputs would identify the memory hogging
culprit.  However, what I got instead was something like:

2500000
8000000
(key):(memory released)
.
.
.
5500000

After unsetting ALL globals within the global context (there are no
stack frames present, and therefore no non-global variables), PHP
still claimed my script was holding onto 5.5MB!!

Note that traverse_hierarchy does not do anything that results in
additional code being evaluated.  There are no new function
definitions, includes, or evals.  So the additional memory is not
being used to store any new function or class definitions.  All
non-global memory allocated by the function (theoretically) goes away
when the function exits, and all globals allocated by the function get
unset by the clear_globals function.  I'm leaking memory somewhere --
the question is where?

I've scoured my own code -- and even tentatively looked at
DB_DataObject -- for cases of circular references, and have found
none.

The question is this: Given the following assumptions:

1) PHP's memory manager reclaims memory when all references to that memory are
gone.
2) A reference is 'gone' when it goes out of scope or is 'unset'.
3) The only references that remain in the global context are
references to globals (all non-global variables have gone out of scope
and that memory reclaimed)
4) $GLOBALS is a PHP special associative array that contains the name and
value of all global variables.
5) By doing unset($GLOBALS[$varname]) and unset($$varname), where $varname is
each key of the $GLOBALS array, I am effectively eliminating all remaining
references, and all allocated memory should be reclaimed by the memory
manager (except perhaps for memory associated with function and class
definitions).
6) Resources (think database resources) are automatically freed by
garbage collection when there are no more references to them
7) No additional code is being evaluated within traverse_hierarchy
8) I'm correct that there aren't any circular references in my code
nor in any PEAR module code

Are there any other ways that user code can result in this apparent
memory leak situation?  If so, what are they?

Or, are any of my first 6 assumptions incorrect?

Thanks,

- Matthew H. North

--
PHP General Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to