PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)。现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完全兼容。PHP5.3在PHP5.2的基础上做了诸多改进,其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3的垃圾回收机制,并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题。
PHP变量及关联内存对象的内部表示
垃圾回收说到底是对变量及其所关联内存对象的操作,所以在讨论PHP的垃圾回收机制之前,先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示)。
PHP官方文档中将PHP中的变量划分为两类:标量类型和复杂类型。标量类型包括布尔型、整型、浮点型和字符串;复杂类型包括数组、对象和资源;还有一个NULL比较特殊,它不划分为任何类型,而是单独成为一类。
所有这些类型,在PHP内部统一用一个叫做zval的结构表示,在PHP源代码中这个结构名称为“_zval_struct”。zval的具体定义在PHP源代码的“Zend/zend.h”文件中,下面是相关代码的摘录。
<ol class="dp-c"> <li class="alt"><span><span>typedef union _zvalue_value { </span></span></li> <li> <span> long lval; </span><span class="comment">/* long value */</span><span> </span> </li> <li class="alt"> <span> double dval; </span><span class="comment">/* double value */</span><span> </span> </li> <li><span> struct { </span></li> <li class="alt"><span> char *val; </span></li> <li><span> int len; </span></li> <li class="alt"><span> } str; </span></li> <li> <span> HashTable *ht; </span><span class="comment">/* hash table value */</span><span> </span> </li> <li class="alt"><span> zend_object_value obj; </span></li> <li><span>} zvalue_value; </span></li> <li class="alt"><span> </span></li> <li><span>struct _zval_struct { </span></li> <li class="alt"> <span> </span><span class="comment">/* Variable information */</span><span> </span> </li> <li><span> zvalue_value value; </span></li> <li class="alt"> <span class="comment">/* value */</span><span> </span> </li> <li><span> zend_uint refcount__gc; </span></li> <li class="alt"> <span> zend_uchar type; </span><span class="comment">/* active type */</span><span> </span> </li> <li><span> zend_uchar is_ref__gc; </span></li> <li class="alt"><span>}; </span></li> </ol>
其中联合体“_zvalue_value”用于表示PHP中所有变量的值,这里之所以使用union,是因为一个zval在一个时刻只能表示一种类型的变量。可以看到_zvalue_value中只有5个字段,但是PHP中算上NULL有8种数据类型,那么PHP内部是如何用5个字段表示8种类型呢?这算是PHP设计比较巧妙的一个地方,它通过复用字段达到了减少字段的目的。例如,在PHP内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(注意PHP中的数组其实是哈希表);而obj存储对象类型;如果所有字段全部置为0或NULL则表示PHP中的NULL,这样就达到了用5个字段存储8种类型的值。
而当前zval中的value(value的类型即是_zvalue_value)到底表示那种类型,则由“_zval_struct”中的type确定。_zval_struct即是zval在C语言中的具体实现,每个zval表示一个变量的内存对象。除了value和type,可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc,从其后缀就可以断定这两个家伙与垃圾回收有关。没错,PHP的垃圾回收全靠这俩字段了。其中refcount__gc表示当前有几个变量引用此zval,而is_ref__gc表示当前zval是否被按引用引用,这话听起来很拗口,这和PHP中zval的“Write-On-Copy”机制有关,由于这个话题不是本文重点,因此这里不再详述,读者只需记住refcount__gc这个字段的作用即可。
PHP5.2中的垃圾回收算法——Reference Counting
PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval,而计数器就是refcount__gc。
例如下面一段PHP代码演示了PHP5.2计数器的工作原理(计数器值通过xdebug得到):
<ol class="dp-c"> <li class="alt"><span><span><?php </span></span></span></li> <li><span> </span></li> <li class="alt"> <span class="vars">$val1</span><span> = 100; </span><span class="comment">//zval(val1).refcount_gc = 1; </span><span> </span> </li> <li> <span class="vars">$val2</span><span> = </span><span class="vars">$val1</span><span>; </span><span class="comment">//zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy,当前val2与val1共同引用一个zval) </span><span> </span> </li> <li class="alt"> <span class="vars">$val2</span><span> = 200; </span><span class="comment">//zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval) </span><span> </span> </li> <li> <span>unset(</span><span class="vars">$val1</span><span>); </span><span class="comment">//zval(val1).refcount_gc = 0($val1引用的zval再也不可用,会被GC回收) </span><span> </span> </li> <li class="alt"><span> </span></li> <li><span>?> </span></li> </ol>
Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露。很多朋友可能已经意识到了,如果存在循环引用,那么Reference Counting就可能导致内存泄露。例如下面的代码:
<ol class="dp-c"> <li class="alt"><span><span><?php </span></span></span></li> <li><span> </span></li> <li class="alt"> <span class="vars">$a</span><span> = </span><span class="keyword">array</span><span>(); </span> </li> <li> <span class="vars">$a</span><span>[] = & </span><span class="vars">$a</span><span>; </span> </li> <li class="alt"> <span>unset(</span><span class="vars">$a</span><span>); </span> </li> <li><span> </span></li> <li class="alt"><span>?> </span></li> </ol>
这段代码首先建立了数组a,然后让a的第一个元素按引用指向a,这时a的zval的refcount就变为2,然后我们销毁变量a,此时a最初指向的zval的refcount为1,但是我们再也没有办法对其进行操作,因为其形成了一个循环自引用,如下图所示: