You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

未调用gc_collect_cycles()时PHP 8.2内存耗尽,是否为Bug?

问题解答:PHP循环引用导致内存耗尽,手动GC解决是否属于Bug?

这不属于PHP的Bug,是循环引用场景下垃圾回收机制的正常行为,下面详细解释原因:

核心原因:PHP循环引用GC的触发逻辑

PHP的垃圾回收分为两种机制:

  1. 引用计数回收:针对普通对象,当引用计数归0时立即回收,但循环引用的对象(比如示例中Foo和Bar互相引用)引用计数永远不会归0,这种情况只能靠第二种机制处理。
  2. 循环引用垃圾回收:专门处理循环引用的对象,但它不是在内存不足时主动触发,而是基于「根缓冲区」的填充量触发——默认根缓冲区容量为10000,只有当缓冲区被填满时,PHP才会启动循环引用GC进行清理。

你的代码中,循环仅执行10次,远未达到根缓冲区的触发阈值,所以循环引用的Foo/Bar实例不会被自动回收,每个实例携带30MB的字符串,10次循环就占用了300MB内存,超过200M的限制后触发内存耗尽错误。而手动调用gc_collect_cycles()时,会强制触发循环引用GC,每次循环后清理掉无用实例,内存不会累积,因此能正常运行。

为什么内存耗尽前不自动触发GC?

PHP在分配内存时会检查是否超出memory_limit,但这个检查流程不会主动启动循环引用GC,原因是:

  • GC运行本身有性能开销,如果每次内存分配失败都触发GC,会导致程序性能大幅下降。
  • PHP的设计目标是平衡性能与内存回收,默认优先保证执行速度,仅在达到特定条件时才启动循环引用GC。

解决方案

  1. 手动触发GC:在循环结束或每次循环后调用gc_collect_cycles(),适合明确知道会产生循环引用的场景。
  2. 调整GC触发阈值:通过php.ini配置gc.max_root_buffer降低触发门槛(比如设为10),但要注意这会增加GC的触发频率,可能影响性能。
  3. 避免循环引用:在对象不再使用时,手动解除循环引用(比如$foo->bar = null;),让引用计数机制可以正常回收内存。

复现代码

运行命令:

php -d memory_limit=200M script.php

代码:

<?php

class Foo {
    private $bar; // Foo和Bar实例间形成循环引用

    public function __construct() {
        $this->bar = new Bar($this);
    }
}

class Bar {
    private $foo; // Foo和Bar实例间形成循环引用
    private $longStr;

    public function __construct($foo) {
        $this->foo = $foo;

        // 分配一个30MB的字符串
        $this->longStr = sprintf("%30000000s", "");
    }
}

function doLoop($forceGc) {
    for ($i = 0; $i < 10; ++$i) {
        $foo = new Foo();

        if ($forceGc) {
            gc_collect_cycles();
        }
    }
}

doLoop(true); // 正常运行
doLoop(false); // 抛出"Fatal error: Allowed memory size of 209715200 bytes exhausted"

内容的提问来源于stack exchange,提问作者Sämy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 09:55:29