最近在写某个脚本时,在循环内重复调用了某个方法。按照以前的理解,方法在执行完成后,局部变量就失效了,它申请内存就释放了,但实际上并非如此。
<?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()。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。