使用腳本語言最大的好處之一就是可利用其擁有的自動垃圾回收機制(釋放記憶體)。你不需要在使用完變數後做任何釋放記憶體的處理,PHP會幫你完成。
當然,我們可以按自己的意願呼叫unset() 函數來釋放內存,但通常不需要這麼做。
不過在PHP裡,至少有一種狀況記憶體不會被自動釋放,即便是手動呼叫unset()。詳情可考: http://bugs.php.net/bug.php?id=33595 。
問題症狀如果兩個物件之間存在著相互引用的關係,如“父物件-子物件”,則對父物件呼叫unset()不會釋放在子物件中引用父物件的記憶體(即便父物件被垃圾回收,也不行)。
有些糊塗了?我們來看下面的這段程式碼:
<?phpclass 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()) . "n";}?>執行這段程式碼,你會看到記憶體使用率越來越高越來越高,直到用光。
...33,551,61633,551,97633,552,33633,552,696PHP Fatal error: Allowed tolocate size of 33554432 bytes exhausted(tried to allocate size of 3354432 bytes exhausted(tried to allocate 16 phptes in memline memline) in memline 1675) in memline 1671on memline。種情況不算是什麼問題。
但如果你在一個長期運行的程式碼中使用到了一大堆相互引用的對象,尤其是在對象相對較大的情況下,內存會迅速地消耗殆盡。
Userland解決方案雖然有些乏味、不優雅,但先前提到的bugs.php.net 連結中提供了一個解決方案。
這個方案在釋放物件前使用一個destructor 方法以達到目的。 Destructor 方法可將所有內部的父物件參考全部清除,也就是說可以將這部分本來會溢位的記憶體釋放掉。
以下是「修正」的程式碼:
<?phpclass Foo {function __construct(){$this->bar = new Bar($this);}function __destruct(){unset($this->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()) . "n";}?>注意那個新增的Foo::__destruct()方法,以及在釋放物件前對$foo->__destruct() 的呼叫。現在這段程式碼解決了記憶體使用率一直增加的問題,這麼一來,程式碼就可以很好的工作了。
PHP內核解決方案?
為什麼會有記憶體溢出的發生?我對PHP內核的研究並不精通,但可以確定的是此問題與引用計數有關。
在$bar 中引用$foo 的引用計數不會因為父對象$foo 被釋放而遞減,這時PHP認為你仍需要$foo 對象,也就不會釋放這部分的內存……大概是這樣。
這裡確實可以看出我的無知,但大體意思是:一個引用計數沒有遞減,所以一些記憶體永遠無法釋放。
在前面提到的bugs.php.net 連結中我看到修改垃圾回收的過程將會犧牲極大的效能,因為我對引用計數了解不多,所以我認為這是真的。
與其改變垃圾回收的過程,為什麼不用unset() 對內部物件做釋放的工作呢? (或是在釋放物件的時候呼叫__destruct()?)
也許PHP核心開發者可以在此或其他地方,對此垃圾回收處理機製做出修改。
更新:Martin Fjordvald 在評論中提到了一個由David Wang 為垃圾回收所寫的補丁(其實它看起來更像“一整塊布”——非常巨大。詳情請參閱此郵件結尾的CVS導出信息。)確實存在(一封郵件),並受到了PHP內核開發成員的關注。問題是這個補丁要不要放到PHP5.3中並未得到太多支援。我覺得一個不錯的折中方案就是在unset() 函數中呼叫物件中的__destruct() 方法;