|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2009-07-02 18:35 UTC] nate at frickenate dot com
Description:
------------
The new cyclical garbage collector isn't collecting everything it should be - somewhere there is a memory leak. The issue I am bringing up is not about delayed collection - this is a true memory leak with memory that is never reclaimed.
When a variable (in this example, $user) contains an object with a cyclical reference, there is a small amount of memory that is leaked/not reclaimed when you set $user to another object (of any class). If you set $user to an int/string/boolean/null before setting $user to another object, all memory is collected properly.
I came to the conclusion that this must be a problem with the garbage collector, since if you replace the "$this->user = $user;" with "$this->user = new StdClass;" (thus preventing the creation of a cyclical reference), the leak goes away.
So basically right now:
<?php
while ($user = get_next_user()) {
// do stuff with $user
// php bug: have to unset before next iteration
// to prevent memory leak caused by buggy gc
unset($user);
}
?>
Reproduce code:
---------------
<?php
/*
How to use:
a) run test 1 by running code as-is
b) run test 2 by commenting out test 1, uncomment test 2
c) run test 3 by commenting out test 1 & 2, uncomment test 3
*/
class User {
protected
$profile;
public function __construct () {
$this->profile = new UserProfile($this);
}
}
class UserProfile {
private
$user;
public function __construct ($user) {
$this->user = $user;
}
}
for ($userid = 1; ; $userid++) {
$user = new User;
if ($userid % 100000 == 0)
printf("memory usage after %s users: %s MB.\n", number_format($userid), number_format(memory_get_usage() / 1024 / 1024, 2));
// TEST 1 - do nothing before we set the
// new User on next iteration of the loop).
// RESULT: leaks memory (see "Actual result" section)
continue;
// TEST 2 - set $user to an empty object, before
// we set the new User on next iteration
// RESULT: leaks memory (see "Actual result" section)
//$user = new StdClass; continue;
// TEST 3 - set $user to anything other than an object,
// before we set the new User on next iteration.
// RESULT: does NOT leak memory (see "Expected result" section)
//$user = 'not an object'; continue;
}
?>
Expected result:
----------------
memory usage after 100,000 users: 1.54 MB.
memory usage after 200,000 users: 1.54 MB.
memory usage after 300,000 users: 1.54 MB.
memory usage after 400,000 users: 1.54 MB.
memory usage after 500,000 users: 1.54 MB.
memory usage after 600,000 users: 1.54 MB.
memory usage after 700,000 users: 1.54 MB.
memory usage after 800,000 users: 1.54 MB.
memory usage after 900,000 users: 1.54 MB.
memory usage after 1,000,000 users: 1.54 MB.
[snip]
memory usage after 99,000,000 users: 1.54 MB.
memory usage after 99,100,000 users: 1.54 MB.
memory usage after 99,200,000 users: 1.54 MB.
memory usage after 99,300,000 users: 1.54 MB.
memory usage after 99,400,000 users: 1.54 MB.
memory usage after 99,500,000 users: 1.54 MB.
memory usage after 99,600,000 users: 1.54 MB.
memory usage after 99,700,000 users: 1.54 MB.
memory usage after 99,800,000 users: 1.54 MB.
memory usage after 99,900,000 users: 1.54 MB.
memory usage after 100,000,000 users: 1.54 MB.
Actual result:
--------------
memory usage after 100,000 users: 1.55 MB.
memory usage after 200,000 users: 1.57 MB.
memory usage after 300,000 users: 1.58 MB.
memory usage after 400,000 users: 1.59 MB.
memory usage after 500,000 users: 1.61 MB.
memory usage after 600,000 users: 1.62 MB.
memory usage after 700,000 users: 1.64 MB.
memory usage after 800,000 users: 1.65 MB.
memory usage after 900,000 users: 1.66 MB.
memory usage after 1,000,000 users: 1.68 MB.
[snip]
memory usage after 99,000,000 users: 18.43 MB.
memory usage after 99,100,000 users: 18.44 MB.
memory usage after 99,200,000 users: 18.46 MB.
memory usage after 99,300,000 users: 18.47 MB.
memory usage after 99,400,000 users: 18.48 MB.
memory usage after 99,500,000 users: 18.50 MB.
memory usage after 99,600,000 users: 18.51 MB.
memory usage after 99,700,000 users: 18.53 MB.
memory usage after 99,800,000 users: 18.54 MB.
memory usage after 99,900,000 users: 18.55 MB.
memory usage after 100,000,000 users: 18.57 MB.
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 03:00:01 2025 UTC |
I confirm, there is a true memory leak. Consider the following script, where the references are unmounted manually with an extra dispose() method: ~~~~ <?php /* How to use: a) run test 1 by running code as-is b) run test 2 by commenting out test 1, uncomment test 2 c) run test 3 by commenting out test 1 & 2, uncomment test 3 */ class User { protected $profile; public function __construct () { $this->profile = new UserProfile($this); } public function dispose() { $this->profile->dispose(); $this->profile = null; } // end dispose(); } class UserProfile { private $user; public function __construct ($user) { $this->user = $user; } public function dispose() { $this->user = null; } // end dispose(); } for ($userid = 1; ; $userid++) { if(isset($user)) { $user->dispose(); } $user = new User; if ($userid % 100000 == 0) printf("memory usage after %s users: %s MB.\n", number_format($userid), number_format(memory_get_usage() / 1024 / 1024, 2)); continue; } ~~~~~ In this case the result is still the same (PHP 5.3.1): ~~~~ memory usage after 100,000 users: 0.61 MB. memory usage after 200,000 users: 0.61 MB. memory usage after 300,000 users: 0.61 MB. memory usage after 400,000 users: 0.61 MB. memory usage after 500,000 users: 0.61 MB. memory usage after 600,000 users: 0.61 MB. memory usage after 700,000 users: 0.61 MB. memory usage after 800,000 users: 0.61 MB. memory usage after 900,000 users: 0.61 MB. memory usage after 1,000,000 users: 0.61 MB. memory usage after 1,100,000 users: 0.61 MB. memory usage after 1,200,000 users: 0.61 MB. ~~~~ If we enable the garbage collector and remove the dispose() method, the used memory level begins to increase, and if we call gc_collect_cycles() after creating a new object, the used memory increases even much faster. On my PC, it is not able to perform display even a single control message after 100000 iterations. It stopped after approx. 33000 users: ~~~~ memory usage after 30,000 users: 25.37 MB. memory usage after 31,000 users: 26.07 MB. memory usage after 32,000 users: 26.76 MB. memory usage after 33,000 users: 31.46 MB. Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 89 bytes) in /home/me/test/memleak.php on line 17 ~~~~I am unable to duplicate this behaviour using 5.3.2. The code given below consistently uses 0.65 MB even up to 10 million iterations - I pasted the output of up to 6 million, with the extended GC reporting turned on: <?php class User { protected $profile; public function __construct () { $this->profile = new UserProfile($this); } public function dispose() { $this->profile->dispose(); $this->profile = null; } } class UserProfile { private $user; public function __construct ($user) { $this->user = $user; } public function dispose() { $this->user = null; } } for ($userid = 1; ; $userid++) { if(isset($user)) { $user->dispose(); } $user = new User; if ($userid % 100000 == 0) printf("memory usage after %s users: %s MB.\n", number_format($userid), number_format(memory_get_usage() / 1024 / 1024, 2)); continue; } ?> memory usage after 100,000 users: 0.65 MB. memory usage after 200,000 users: 0.65 MB. memory usage after 300,000 users: 0.65 MB. [snip] memory usage after 5,700,000 users: 0.65 MB. memory usage after 5,800,000 users: 0.65 MB. memory usage after 5,900,000 users: 0.65 MB. GC Statistics ------------- Runs: 0 Collected: 0 Root buffer length: 0 Root buffer peak: 9 Possible Remove from Marked Root Buffered buffer grey -------- -------- ----------- ------ ZVAL 16 7 7 0 ZOBJ 77999984 11999998 11999998 0The gc cycle seems to miss the last 2 items on this cycle, but picks up from the last one for the next cycle, so is only actually dropping 1 item per cycle. My cycle goes something like this ... Create and assign #1 to #4999 Create #5000 Destroy #1 to #4998 Assign #5000 Create and assign #5001 to #9999 Create #10000 Destroy #5000 to #9998 --- Skipped #4999 Assign #10000 Create and assign #10001 to #14999 Create #15000 Destroy #10000 to #14998 --- Skipped #9999 etc. My code changes ... class User { protected $profile, $usercount; public function __construct ($usercount) { echo str_pad($this->usercount = $usercount, 5, '0', STR_PAD_LEFT), ':#1', PHP_EOL; $this->profile = new UserProfile($this); } public function __destruct() { echo str_pad($this->usercount, 5, '0', STR_PAD_LEFT), ':#3', PHP_EOL; } } class UserProfile { private $user; public function __construct ($user) { $this->user = $user; } } for ($userid = 1; ; $userid++) { $user = new User($userid); echo str_pad($userid, 5, '0', STR_PAD_LEFT), ':#2', PHP_EOL; // Same code as provided by Nate.