PHP中的循环引用是内存泄漏的常见原因。当对象直接或间接地相互引用时,就会发生循环引用。幸运的是,PHP有一个垃圾收集器可以检测和清理循环引用。但是,这会消耗CPU周期并可能减慢应用程序的速度。
垃圾收集器会在内存中存在10,000个可能的循环对象或数组,并且其中一个超出作用域时触发。
如果您有少量使用大量内存的对象,则永远不会触发垃圾收集。即使内存被垃圾收集器应该收集的孤立对象使用,您也可能达到内存限制。
这就是为什么您应该识别创建循环引用的情况并避免它们的原因。
理想情况下,对于Web应用程序,您希望禁用垃圾收集器,并在发送响应后让PHP释放所有内存。但这对于长时间运行的脚本(例如守护进程或工作进程)来说是危险的,因为内存泄漏会随着时间的推移而累积,并通过频繁调用垃圾收集器来减慢应用程序的速度。
在本文中,我们将探讨闭包和生成器如何保存循环引用以及如何防止它们。
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { public function __construct(public A $a) {} }</code>
在此示例中,A和B相互引用。当您创建A的实例时,它会创建一个引用A的B实例。这会创建一个循环引用。
为了检测循环引用,我们可以使用gc_collect_cycles()
手动触发垃圾收集器,并使用gc_status()
读取收集到的引用的数量。
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status());</code>
这将输出:
<code>Array ( ... [collected] => 2 ... )</code>
此示例表明垃圾收集器已检测到并删除了2个具有循环引用的对象。
您还可以使用xdebug_debug_zval()
函数查看对象的引用数量。
当遇到循环引用时,一个简单的解决方案是使用弱引用。弱引用是一个对象,它持有的引用不会阻止垃圾收集器收集它引用的对象。在PHP中,您可以使用WeakReference
类创建弱引用。
这需要对代码进行一些更改。B类现在存储WeakReference
对象而不是A对象。您必须使用WeakReference
对象的get()
方法访问A对象。
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { /** @var WeakReference<a> $a */ public WeakReference $a; public function __construct(A $a) { $this->a = WeakReference::create($a); } }</code>
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 0</code>
在输出中,您将看到收集到的引用数量现在为0。
提示1:仅在必要时使用弱引用来防止循环引用。
PHP中闭包的概念是创建一个可以访问父作用域中变量的函数。如果您不小心,这可能会导致循环引用。
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { public function __construct(public A $a) {} }</code>
在此示例中,闭包$a->b
引用父作用域中的变量$a
。循环引用很容易发现,因为引用是明确的。
但是,如果您使用闭包的简写语法,则可能会以更隐蔽的方式出现相同的问题。使用箭头函数,变量$a
不会在闭包中显式引用,但它仍然被按引用捕获。
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status());</code>
在此示例中,收集到的引用数量为2,表明存在循环引用。
在类方法中创建的任何非静态闭包都将对对象实例($this
)具有引用,即使没有访问$this
也是如此。
<code>Array ( ... [collected] => 2 ... )</code>
这是因为$this
引用始终在闭包中按引用捕获。可以使用Reflection::getClosureThis()
访问它。
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { /** @var WeakReference<a> $a */ public WeakReference $a; public function __construct(A $a) { $this->a = WeakReference::create($a); } }</code>
如果从全局作用域或静态方法中创建闭包,则$this
引用为null。
提示2:如果您不需要
$this
,则始终使用static function () {}
或static fn () =>
来创建闭包。
我们来说说这篇文章的原因。我最近发现了一些东西: 生成器会保留引用,只要它们没有被耗尽。
在此示例中,该类将生成器存储在一个属性中,但生成器对对象实例具有$this
引用。
生成器表现得像一个闭包,并保留对对象实例的引用。
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 0</code>
类实例被垃圾收集器收集,因为它对生成器有引用,而生成器对对象实例有引用。
一旦生成器被耗尽,引用就会被释放,对象实例就会从内存中删除。
<code class="language-php">function createCircularReference() { $a = new stdClass(); $a->b = function () use ($a) { return $a; }; return $a; }</code>
提示3:通过迭代始终耗尽生成器。
提示4:使用静态方法或闭包来创建生成器,以免保留对对象实例的引用。
循环引用是PHP中内存泄漏的常见原因。即使垃圾收集器可以检测和清理循环引用,它也会消耗CPU周期并可能减慢应用程序的速度。您必须检测创建此类循环引用的情况并调整代码以防止它们。使用弱引用可以防止循环引用,但一些简单的技巧可以帮助您首先防止循环引用:
$this
,则使用static function () {}
或static fn () =>
来创建闭包。以上是PHP 闭包和生成器可以保存循环引用的详细内容。更多信息请关注PHP中文网其他相关文章!