当前位置: 首页 > 科技观察

PHP的垃圾回收机制——引用计数

时间:2023-03-13 19:27:05 科技观察

每一个php变量都存在于一个名为“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值外,还包含两个字节的附加信息。最后一个是“is_ref”,它是一个bool值,用来标识这个变量是否属于一个引用集(referenceset)。通过这个字节,php引擎可以区分普通变量和引用变量。由于php允许用户通过使用&来自定义引用,所以在zval变量容器中也有一个内部引用计数机制来优化内存使用。第二个多出来的字节是“refcount”,用来表示指向这个zval变量容器的变量(也叫symbols或symbols)的个数。所有的符号都存在于一个符号表中,每个符号表都有一个作用域(scope),那些主要的脚本(如:浏览器请求的脚本)和每个函数或方法也有一个作用域。当一个变量被赋予常量值时,会生成一个zval变量容器,如下例:例1生成一个新的zval容器上面的例子中,新建的变量a,在当前范围内生成。并生成一个string类型,值为newstring的变量容器。在额外的两个字节信息中,“is_ref”默认设置为FALSE,因为不会生成自定义引用。“refcount”设置为1,因为只有一个变量使用这个变量容器。请注意,当“refcount”为1时,“is_ref”始终为FALSE。如果你已经安装了?Xdebug,你可以通过调用函数xdebug_debug_zval()来显示“refcount”和“is_ref”的值。示例2显示zval信息上面的例程会输出:a:(refcount=1,is_ref=0)='newstring'将一个变量赋值给另一个变量会增加引用数(refcount).Example3添加一个zval引用计数上面的例程将输出:a:(refcount=2,is_ref=0)='newstring'此时引用数为2,因为同一个变量容器关联了变量a和变量b。PHP在不需要的时候不会复制生成的变量容器。当“refcount”变为0时,变量容器被销毁。当与变量容器关联的任何变量离开其作用域时(例如:函数执行结束),或者在变量上调用函数unset()时,“refcount”它会减1,如下例可以说明:Example4Reducereferencecount上面的例程将输出:a:(refcount=3,is_ref=0)='newstring'a:(refcount=1,is_ref=0)='newstring'如果我们现在执行unset($a);并且包含类型和值的变量容器将从内存中删除。复合类型当您考虑数组和对象等复合类型时,事情会变得有点复杂。与标量类型的值不同,数组和对象类型的变量将它们的成员或属性存储在它们自己的符号表中。这意味着下面的示例将生成三个zval变量容器。示例5创建数组zval'life','number'=>42);xdebug_debug_zval('a');?>以上例程的输出类似于:a:(refcount=1,is_ref=0)=array('meaning'=>(refcount=1,is_ref=0)='life','number'=>(refcount=1,is_ref=0)=42)icon:简单数组的zval三个zval变量容器是:a,meaning和number。递增和递减“refcount”的规则与上述相同。接下来,我们向数组中添加另一个元素并将其值设置为数组中现有元素的值:示例6添加现有元素到数组中'life','number'=>42);$a['life']=$a['meaning'];xdebug_debug_zval('a');?>上述例程的输出类似于:a:(refcount=1,is_ref=0)=array('meaning'=>(refcount=2,is_ref=0)='life','number'=>(refcount=1,is_ref=0)=42,'life'=>(refcount=2,is_ref=0)='life')插图:zvalwithasimplearrayofreferences从上面的xdebug输出,我们看到了原文数组元素和新添加的数组元素关联到“refcount”2的同一个zval变量容器。虽然Xdebug的输出显示了两个值为'life'的zval变量容器,但它们实际上是同一个容器。函数xdebug_debug_zval()不显示这些信息,但是通过显示内存指针信息可以看到。删除数组中的元素类似于从范围中删除变量。删除后,数组中元素所在容器的“refcount”值减少。同样,当“refcount”为0时,变量container从内存中删除,下面的另一个例子可以说明:例7从数组中删除一个元素'life','number'=>42);$a['life']=$a['meaning'];unset($a['meaning'],$a['number']);xdebug_debug_zval('a');?>以上例程的输出类似于:a:(refcount=1,is_ref=0)=array('life'=>(refcount=1,is_ref=0)='life')现在,当我们添加一个数组本身作为这个数组的一个元素,事情就变得有趣了,下一个例子会说明这一点。在示例中我们添加了引用运算符,否则php会生成一个副本。示例8将数组作为元素添加到自己上述例程的输出类似于:一个:(refcount=2,is_ref=1)=array(0=>(refcount=1,is_ref=0)='one',1=>(refcount=2,is_ref=1)=...)图标:自引用(curcularreference,是自身元素之一)的数组的zval可以看到数组变量(a)指向的变量容器中的“refcount”也是第二个元素(1)数组的值为2。上面输出中的“...”表示发生了递归,这显然意味着在这种情况下“...”指向原始数组。和之前一样,对一个变量调用unset会删除这个符号,它指向的变量容器中的引用计数也会减1。所以,如果我们在执行完上面的代码后对变量$a调用unset,那么变量$a的引用计数和数组元素“1”指向的变量容器都会减1,从“2”减为“1””。下面的例子可以说明:Example9Unsetting$a(refcount=1,is_ref=1)=array(0=>(refcount=1,is_ref=0)='one',1=>(refcount=1,is_ref=1)=...)图:删除带有循环引用的数组后的Zvals演示了内存泄漏清理问题尽管在某个范围内不再有任何符号指向此结构(即变量container),因为数组元素“1”仍然指向数组本身,这个容器不能被清除。由于没有其他符号指向它,用户没有办法清理这个结构,导致内存泄漏。幸运的是,php会在脚本执行结束时清除这个数据结构,但是在php清除之前,会消耗大量内存。如果您正在实施分析算法,或者做其他事情,比如孩子指向其父母,这种情况会经常发生。当然,对象也会发生同样的情况,而且实际上更可能发生在对象身上,因为对象总是被隐式引用。上面的情况如果只出现一两次还好,但是如果有几千甚至几十万次内存泄漏,这显然是个大问题。此类问题往往发生在长时间运行的脚本中,例如请求很少结束的守护进程,或者单元测试中的大集合。后者的一个例子:在对庞大的eZ(著名的PHPLibrary)组件库的模板组件进行单元测试时,可能会出现问题。有时测试可能需要消耗2GB内存,而测试服务器很可能没有这么大的内存。