首页 >后端开发 >php教程 >PHP 闭包和生成器可以保存循环引用

PHP 闭包和生成器可以保存循环引用

Patricia Arquette
Patricia Arquette原创
2025-01-18 06:03:09353浏览

PHP Closures and Generators can hold circular references

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)具有引用,即使没有访问$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中的垃圾收集是什么?如何充分利用它?
  • memprof——PHP的内存分析器。帮助查找PHP脚本中的内存泄漏。
  • Xdebug的内置分析器

以上是PHP 闭包和生成器可以保存循环引用的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
上一篇:Neighboring Bitwise XOR下一篇:暂无