未调用gc_collect_cycles()时PHP 8.2内存耗尽,是否为Bug?
问题解答:PHP循环引用导致内存耗尽,手动GC解决是否属于Bug?
这不属于PHP的Bug,是循环引用场景下垃圾回收机制的正常行为,下面详细解释原因:
核心原因:PHP循环引用GC的触发逻辑
PHP的垃圾回收分为两种机制:
- 引用计数回收:针对普通对象,当引用计数归0时立即回收,但循环引用的对象(比如示例中Foo和Bar互相引用)引用计数永远不会归0,这种情况只能靠第二种机制处理。
- 循环引用垃圾回收:专门处理循环引用的对象,但它不是在内存不足时主动触发,而是基于「根缓冲区」的填充量触发——默认根缓冲区容量为10000,只有当缓冲区被填满时,PHP才会启动循环引用GC进行清理。
你的代码中,循环仅执行10次,远未达到根缓冲区的触发阈值,所以循环引用的Foo/Bar实例不会被自动回收,每个实例携带30MB的字符串,10次循环就占用了300MB内存,超过200M的限制后触发内存耗尽错误。而手动调用gc_collect_cycles()时,会强制触发循环引用GC,每次循环后清理掉无用实例,内存不会累积,因此能正常运行。
为什么内存耗尽前不自动触发GC?
PHP在分配内存时会检查是否超出memory_limit,但这个检查流程不会主动启动循环引用GC,原因是:
- GC运行本身有性能开销,如果每次内存分配失败都触发GC,会导致程序性能大幅下降。
- PHP的设计目标是平衡性能与内存回收,默认优先保证执行速度,仅在达到特定条件时才启动循环引用GC。
解决方案
- 手动触发GC:在循环结束或每次循环后调用
gc_collect_cycles(),适合明确知道会产生循环引用的场景。 - 调整GC触发阈值:通过
php.ini配置gc.max_root_buffer降低触发门槛(比如设为10),但要注意这会增加GC的触发频率,可能影响性能。 - 避免循环引用:在对象不再使用时,手动解除循环引用(比如
$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
相关产品推荐
相关产品推荐

