php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #69639 Closure causes memory leak (Cyclic reference) when used as class property
Submitted: 2015-05-15 10:15 UTC Modified: 2016-12-17 14:43 UTC
From: tom at r dot je Assigned:
Status: Duplicate Package: Performance problem
PHP Version: 5.6.9 OS: *
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: tom at r dot je
New email:
PHP Version: OS:

 

 [2015-05-15 10:15 UTC] tom at r dot je
Description:
------------
Because closures inside classes implicitly have a reference to $this, if a closure is used as a class property, this automatically creates a cyclic reference.

Having read the page on GC here: http://php.net/manual/en/features.gc.collecting-cycles.php I can understand the problem, but the issue this causes is a memory leak any time a closure is used as a class property (Or in an array/data structure in a class property)

Consider the following code:

<?php
class X {
	private $largeStr;
	private $closure;

	public function foo() {
		$this->largestr = str_repeat('ABC', 10000000);

		$this->closure = function() {};
	}
}




echo (memory_get_usage()/1024/1024) . 'mb<br >';

for ($i = 0; $i < 1000; $i++) {
	$x = new X;
	$x->foo();
	unset($x);
	echo (memory_get_peak_usage()/1024/1024) . 'mb<br >';
}

?>

Which outputs:

0.21446228027344mb
28.827682495117mb
57.438827514648mb
86.050003051758mb
114.66118621826mb

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 30000001 bytes)


I have explicitly called unset() which I would hope would free the memory used by the class, clearly it doesn't. Similarly, adding 


	public function __destruct() {
		unset($this->closure);
	}

to the class does not help.



I'm not sure there is an obvious fix for this but gc_collect_cycles() in the loop does solve it, I have to wonder why unset() doesn't just trigger gc_collect_cycles(). Presumably it can do it in a smarter way as well because it doesn't have to look through every defined variable.




Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-05-15 11:29 UTC] cmb@php.net
-Status: Open +Status: Verified
 [2015-05-15 11:29 UTC] cmb@php.net
As explained on the mentioned manual page, the garbage collection
is only triggered when the root buffer runs full. Its default size
is 10,000, so in your example the garbage collection is likely to
be never executed, because only 1,000 objects are created.

However, increasing the number of created objects by factor 100 to
100,000 and reducing the length of $largestr by the same factor,
results in the same behavior, what appears to be a bug.
 [2015-05-15 11:43 UTC] nikic@php.net
Root issue is bug #60982: We do not run an automatic cycle collection on OOM or memory limit events.
 [2016-12-17 14:43 UTC] nikic@php.net
-Status: Verified +Status: Duplicate
 [2016-12-17 14:43 UTC] nikic@php.net
Closing as duplicate of the references bug, as this is a general GC issue not related to closures.
 
PHP Copyright © 2001-2020 The PHP Group
All rights reserved.
Last updated: Tue Jan 28 22:01:25 2020 UTC