最近在寫某個腳本時,在循環內重複調用了某個方法。按照以前的理解,方法在執行完成後,局部變量就失效了,它申請內存就釋放了,但實際上並非如此。
<?php
class Foo
{
public $var = '3.1415962654';
}
$baseMemory = memory_get_usage();
for ( $i = 0; $i <= 100000; $i++ )
{
f($i, $baseMemory);
}
function f($i, $baseMemory)
{
$a = new Foo;
$a->self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
運行上面這段代碼後發現,php的內存並不是離開函數就釋放,而是達到一定值後才會進行釋放(只討論php5.3之後的機制)。官方的説法是
首先,實現垃圾回收機制的整個原因是為了,一旦先決條件滿足,通過清理循環引用的變量來節省內存佔用。在PHP執行中,一旦根緩衝區滿了或者調用gc_collect_cycles() 函數時,就會執行垃圾回收。
也就是説只要根緩衝區滿了,php就會執行垃圾回收,釋放那些沒用到的內存。
那麼什麼是“根緩衝區”呢?根緩衝區就是拿來存放所有可能根(可以理解為php裏的變量)的容器,他的值是10000,可以修改PHP源碼文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然後重新編譯PHP,來修改這個10000值。
如果沒有修改過根緩衝區的值,觀察上面的代碼就會發現,每10000次,就會執行一次垃圾回收,也就是根緩衝區在第一萬次的時候被填滿了。
那麼問題就來了,如果我的單個變量佔內存比較大,那麼根緩衝區還沒填滿,就有可能把內存用完了,也就來不及重新分配內存,這就是可能導致內存泄漏的原因之一。比如下面這個例子
<?php
ini_set('memory_limit', '128M');
class Use10MClass
{
public $var = null;
public function __construct()
{
$this->var = str_pad('1', 10 * 1024 * 1024);
}
}
$baseMemory = memory_get_usage();
echo "當前內存:", memory_get_usage(), "\n";
for ($i = 0; $i <= 100; $i++) {
test($i, $baseMemory);
}
function test($i, $baseMemory)
{
$b = new Use10MClass();
$b->self = $b;
echo sprintf('%8d: ', $i), memory_get_usage() - $baseMemory, "\n";
}
在第十一次循環時,就報了PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to all
ocate 10485785 bytes) 這個錯誤,由於單個變量所消耗的內存過多,根緩衝區才被填了11個,還沒來得及執行垃圾回收內存就被撐爆了。解決方法有兩種
- 財大氣粗的,直接加大分配內存🐶。但是這種方法一般用於應急使用,因為出現內存泄露,基本代表程序多多少少有些問題,最好是找到內存使用過多的原因。
- 在適當的時候調用gc_collect_cycles()主動進行垃圾回收,釋放多餘的空間。
所以上面的代碼最好的解決方法就是,隔一段時間就進行一次手動的垃圾回收。這樣程序就能順利跑完了。
<?php
ini_set('memory_limit', '128M');
class Use10MClass
{
public $var = null;
public function __construct()
{
$this->var = str_pad('1', 10 * 1024 * 1024);
}
}
$baseMemory = memory_get_usage();
echo "當前內存:", memory_get_usage(), "\n";
for ($i = 0; $i <= 100; $i++) {
test($i, $baseMemory);
// 每八次進行一下垃圾回收
if ($i % 8 === 0) {
gc_collect_cycles();
}
}
function test($i, $baseMemory)
{
$b = new Use10MClass();
$b->self = $b;
echo sprintf('%8d: ', $i), memory_get_usage() - $baseMemory, "\n";
}
另外,unset變量並不會立即釋放內存,該溢出的時候還是會溢出的。在函數最後一句unset局部變量是沒有意義的。
總的來説就是變量雖然無法使用了,但是他所佔用的內存空間並沒有被佔用。一般的程序等待根緩衝區滿了,自動垃圾回收就可以了。但是對一些變量比較大的情況,可以在適當的時候執行gc_collect_cycles()主動進行垃圾回收,避免內存泄露。程序進行垃圾回收是會消耗一定的時間的,所以也不推薦頻繁調用gc_collect_cycles()。