腳本語言的自動垃圾回收(GC)機制讓開發者無需手動管理記憶體,但 PHP 至少存在一種情況會導致記憶體無法自動釋放,即便手動呼叫 unset() 也無效。官方 Bug 追蹤:#33595。
當兩個物件形成「父-子」相互引用關係時,釋放父物件不會釋放子物件中對父物件的引用佔用的記憶體。即使父物件被 GC 回收,記憶體仍殘留。
<?php
class Foo {
function __construct() {
$this->bar = new Bar($this);
}
}
class Bar {
function __construct($foo = null) {
$this->foo = $foo;
}
}
while (true) {
$foo = new Foo();
unset($foo);
echo number_format(memory_get_usage()) . " ";
}
?>
結果:記憶體持續增長直至耗盡:
...33,551,616 33,551,976 33,552,336 33,552,696 PHP Fatal error: Allowed memory size of 33554432 bytes exhausted...
對多數短生命週期的 Web 請求影響不大,但長期運行的程序(如守護進程、隊列消費者)若大量使用相互引用的大型物件,記憶體會迅速耗盡。
在釋放物件前,透過 __destruct() 釋放所有內部父物件引用:
<?php
class Foo {
function __construct() {
$this->bar = new Bar($this);
}
function __destruct() {
unset($this->bar); // 關鍵:解除對 Bar 的引用
}
}
class Bar {
function __construct($foo = null) {
$this->foo = $foo;
}
}
while (true) {
$foo = new Foo();
$foo->__destruct(); // 顯式呼叫
unset($foo);
echo number_format(memory_get_usage()) . " ";
}
?>
新增 Foo::__destruct() 並在釋放前呼叫,即可穩定記憶體使用量。
PHP 核心使用參考計數管理記憶體。$bar 內部對 $foo 的引用計數不會因 $foo 被釋放而遞減,導致 PHP 認定 $foo 仍被引用,拒絕釋放記憶體。修改 GC 流程雖可根治,但會犧牲大量效能,官方傾向建議開發者在使用層面解決。
__destruct() 清理內部引用。gc_enable() 並定期呼叫 gc_collect_cycles()(PHP 5.3+)。class Fruit {
protected $con = array();
function aa() {
$this->con[0] = new Apple();
$this->con[1] = new Banana();
echo $this->con[0]->sweet;
}
}
此處 Fruit 持有 Apple、Banana 實例,屬於單向引用,不會導致循環引用問題。
可嘗試將可疑物件引用改為弱引用(WeakReference),有效緩解記憶體洩漏。