PHP垃圾回收机制

某些语言,最典型的如C,需要你显式地要求分配内存当你创建数据结构。一旦你分配到内存,就可以在变量中存储信息。同时你也需要在结束使用变量时释放内存,这使机器可以空出内存给其它变量,避免耗光内存。

PHP可以自动进行内存管理,清除不再需要的对象。PHP使用了引用计数(reference counting)这种单纯的垃圾回收(garbage collection)机制。每个对象都内含一个引用计数器,每个reference连接到对象,计数器加1。当reference离开生存空间或被设为 NULL,计数器减1。当某个对象的引用计数器为零时,PHP知道你将不再需要使用这个对象,释放其所占的内存空间。

一、PHP 垃圾回收机制(Garbage Collector 简称GC)

在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP 的GC垃圾处理机制,防止内存溢出。

当一个 PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。GC进程一般都跟着每起一个SESSION而开始运行的.gc目的是为了在session文件过期以后自动销毁删除这些文件.

二、__destruct /unset
__destruct() 析构函数,是在垃圾对象被回收时执行。

unset 销毁的是指向对象的变量,而不是这个对象。

三、 Session 与 GC
由于PHP的工作机制,它并没有一个daemon线程来定期的扫描Session 信息并判断其是否失效,当一个有效的请求发生时,PHP 会根据全局变量 session.gc_probability 和session.gc_divisor的值,来决定是否启用一个GC, 在默认情况下, session.gc_probability=1, session.gc_divisor =100 也就是说有1%的可能性启动GC(也就是说100个请求中只有一个gc会伴随100个中的某个请求而启动).

GC 的工作就是扫描所有的Session信息,用当前时间减去session最后修改的时间,同session.gc_maxlifetime参数进行比较,如果生存时间超过gc_maxlifetime(默认24分钟) ,就将该session删除。

但是,如果你Web服务器有多个站点,多个站点时,GC处理session可能会出现意想不到的结果,原因就是:GC在工作时,并不会区分不同站点的session.

那么这个时候怎么解决呢?

1. 修改session.save_path,或使用session_save_path() 让每个站点的session保存到一个专用目录,

2. 提供GC的启动率,自然,GC的启动率提高,系统的性能也会相应减低,不推荐。

3. 在代码中判断当前session的生存时间,利用session_destroy()删除.

原理详细分析:
php采用的内存管理和垃圾回收方法是基于引用计数的。在zval结构里有一个refcount是表示引用计数,还有一个is_ref表示是否是个引用变量。那么php代码的实际运行中,又是如何处理的呢?

比如这样的php代码:
$a = “hello”;
$b = $a;
这时候并不像很多人认为的那样,在内存里把”hello”这个字符串复制了一份,而只是把$b指向了和$a对应的同一个zval,然后把那个zval的 refcount + 1。这样避免了一次内存拷贝。但如果在这之后改变了其中一个变量的值,比如$b.= ” world”;又会如何呢?这时候才会分配一个新的zval给$b,然后把原先那个zval的refcount – 1。这就是传说中的copy on write。就是说,在改变值得时候才会有内存拷贝。

那么引用变量又会如何呢? 比如
$a = “hello”;
$b = &$a;
和前面一样,$a, $b还是指向同一个zval。只是还要把这个zval的is_ref置为1。之后再改变$a或者$b的时候就不会再发生拷贝。那么
$a = “hello”;
$b = &$a;
$c = $a;
这时又会如何呢?因为$c并不是一个引用变量,因此不能和$a, $b共用一个zval。因此在$c = $a的时候会直接产生一个新的zval。

因此,在php中,使用引用对改善性能并不会有多少作用,通常情况下还会使情况更糟。所以,引用还是只在真正需要的时候才用为好。

再说说垃圾回收。每个zval都有一个refcount表示它的变量的引用数。不管对于普通变量还是引用变量都是如此。refcount的初始值一般为1。每当增加一个引用时就+1,减少一个引用,比如unset时就会-1。当refcount为0的时候,php就会把它释放掉。这就是基于引用计数的垃圾回收方法。

使用zval

初始化zval
MAKE_STD_ZVAL(zval*);
这个宏的左右是创建一个zval,完成初始化(如将ref_count置为1,isref置为false)并把指针赋给参数。

赋值
写扩展的时候不可避免的要用到把一个zval复制到另一个zval,就是类似$a = $b;的操作。对于简单的值或许手动维护引用计数之类的还不算很麻烦但对于数组,对象之类的就需要一层层递归进去,因此就有了一个zval_copy_ctor来做着件事情。
原有一个zval* p_zval_b,
zval* p_zval_a;
MAKE_STD_ZVAL(p_zval_a); //初始化p_zval_a
*p_zval_a = *p_zval_b;
zval_copy_ctor(p_zval_a);
这里,zval_copy_ctor完成了类似赋值的操作,包括引用计数处理,对于hash值的成员处理等。

释放一个zval则是使用zval_ptr_dtor(**zval)。注意它的参数。它会释放掉为这个zval所分配的内存。

一个实例:

<?php
class ObjectTest{}
function funTest(){}
$object1 = new ObjectTest();    // 建立一个新对象:  引用计数    Reference count = 1
$object2 = $object1;    // 通过引用复制:  Reference count = 2
unset($object);        // 删除一个引用: Reference count = 1
funTest($object2);     
// 通过引用传递对象:   
// 在函数执行期间:
//  Reference count = 2
// 执行结束后:
// Reference count = 1
unset($object2);    // 删除引用: Reference count = 0 自动释放内存空间
?>

PHP对象相互引用的内存溢出
使用脚本语言最大的好处之一就是可利用其拥有的自动垃圾回收机制(释放内存)。你不需要在使用完变量后做任何释放内存的处理,PHP会帮你完成。
当然,我们可以按自己的意愿调用 unset() 函数来释放内存,但通常不需要这么做。
不过在PHP里,至少有一种情况内存不会得到自动释放,即便是手动调用 unset()。详情可考:http://bugs.php.net/bug.php?id=33595。

问题症状
如果两个对象之间存在着相互引用的关系,如“父对象-子对象”,对父对象调用 unset() 不会释放在子对象中引用父对象的内存(即便父对象被垃圾回收,也不行)。
有些糊涂了?我们来看下面的这段代码:

<?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()) . "\n";
}
?>

运行这段代码,你会看到内存使用率越来越高越来越高,直到用光光。

…33,551,61633,551,97633,552,33633,552,696PHP Fatal error: Allowed memory size of 33554432 bytes exhausted(tried to allocate 16 bytes) in memleak.php on line 17对大部分PHP程序员来讲这种情况不算是什么问题。
可如果你在一个长期运行的代码中使用到了一大堆相互引用的对象,尤其是在对象相对较大的情况下,内存会迅速地消耗殆尽。

Userland解决方案
虽然有些乏味、不优雅,但之前提到的 bugs.php.net 链接中提供了一个解决方案。
这个方案在释放对象前使用一个 destructor 方法以达到目的。Destructor 方法可将所有内部的父对象引用全部清除,也就是说可以将这部分本来会溢出的内存释放掉。
以下是“修复后”的代码:

<?php
class 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() 方法;

参考资料:

http://bugs.php.net/bug.php?id=33595

http://hi.baidu.com/bossyt/blog/item/7965cafe556ae5395c60088c.html

http://syre.blogbus.com/logs/15765909.html

http://bbs.ctocio.com.cn/thread-7826271-1-1.html

http://phpchan.bokee.com/viewdiary.24605482.html

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s