您当前的位置: 首页 >  php

PHP新的垃圾回收机制:Zend GC详解

发布时间:2012-11-17 22:11:16 ,浏览量:0

概述

    在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refcount为0,那么变量的空间可以被释放,否则就不释放,这是一种非常简单的GC实现。然而在这种简单的GC实现方案中,出现了意想不到的变量内存泄漏情况(Bug:http://bugs.php.net/bug.php?id=33595),引擎将无法回收这些内存,于是在PHP5.3中出现了新的GC,新的GC有专门的机制负责清理垃圾数据,防止内存泄漏。本文将详细的阐述PHP5.3中新的GC运行机制。

    目前很少有详细的资料介绍新的GC,本文将是目前国内最为详细的从源码角度介绍PHP5.3中GC原理的文章。其中关于垃圾产生以及算法简介部分由笔者根据手册翻译而来,当然其中融入了本人的一些看法。手册中相关内容:Garbage Collection

    在介绍这个新的GC之前,读者必须先了解PHP中变量的内部存储相关知识,请先阅读 变量的内部存储:引用和计数

什么算垃圾

    首先我们需要定义一下“垃圾”的概念,新的GC负责清理的垃圾是指变量的容器zval还存在,但是又没有任何变量名指向此zval。因此GC判断是否为垃圾的一个重要标准是有没有变量名指向变量容器zval。

    假设我们有一段PHP代码,使用了一个临时变量$tmp存储了一个字符串,在处理完字符串之后,就不需要这个$tmp变量了,$tmp变量对于我们来说可以算是一个“垃圾”了,但是对于GC来说,$tmp其实并不是一个垃圾,$tmp变量对我们没有意义,但是这个变量实际还存在,$tmp符号依然指向它所对应的zval,GC会认为PHP代码中可能还会使用到此变量,所以不会将其定义为垃圾。

    那么如果我们在PHP代码中使用完$tmp后,调用unset删除这个变量,那么$tmp是不是就成为一个垃圾了呢。很可惜,GC仍然不认为$tmp是一个垃圾,因为$tmp在unset之后,refcount减少1变成了0(这里假设没有别的变量和$tmp指向相同的zval),这个时候GC会直接将$tmp对应的zval的内存空间释放,$tmp和其对应的zval就根本不存在了。此时的$tmp也不是新的GC所要对付的那种“垃圾”。那么新的GC究竟要对付什么样的垃圾呢,下面我们将生产一个这样的垃圾。  

顽固垃圾的产生过程

    如果读者已经阅读了变量内部存储相关的内容,想必对refcount和isref这些变量内部的信息有了一定的了解。这里我们将结合手册中的一个例子来介绍垃圾的产生过程:

class Foo {     public $var = '3.1415962654'; } $baseMemory = memory_get_usage(); for ( $i = 0; $i <= 100000; $i++ ) {     $a = new Foo;     $a->self = $a;     if ( $i % 500 === 0 )     {         echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "/n";     } } ?>

这段代码的循环体中,新建了一个对象变量,并且用对象的一个成员指向了自己,这样就形成了一个循环引用,当进入下一次循环的时候,又一次给对象变量重新赋值,这样会导致之前的对象变量内存泄漏,在这个例子里面有两个变量泄漏了,一个是对象本身,另外一个是对象中的成员self,但是这两个变量只有对象会作为垃圾收集器的节点被放入缓冲区(因为重新赋值相当于对它进行了unset操作,满足前面的准则3)。在这里我们进行了100,000次循环,而GC在缓冲区中有10,000节点的时候会启动垃圾分析算法,所以这里一共会进行10次的垃圾分析算法。从图中可以清晰的看到,在5.3版本PHP中,每次GC的垃圾分析算法被触发后,内存会有一个明显的减少。而在5.2版本的PHP中,内存使用量会一直增加。

2:运行效率影响

    启用了新的GC后,垃圾分析算法将是一个比较耗时的操作,手册中给了一段测试代码:

class Foo {     public $var = '3.1415962654'; } for ( $i = 0; $i <= 1000000; $i++ ) {     $a = new Foo;     $a->self = $a; } echo memory_get_peak_usage(), "/n"; ?>
然后分别在GC开启和关闭的情况下执行这段代码:
time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php # and time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
最终在该机器上,第一次执行大概使用10.7秒,第二次执行大概使用11.4秒,性能大约降低7%,不过内存的使用量降低了98%,从931M降低到了10M。当然这并不是一个比较科学的测试方法,但是也能说明一定的问题。这种代码测试的是一种极端恶劣条件,实际代码中,特别是在WEB的应用中,很难出现大量循环引用,GC的分析算法的启动不会这么频繁,小规模的代码中甚至很少有机会启动GC分析算法。
总结:
当GC的垃圾分析算法执行的时候,PHP脚本的效率会受到一定的影响,但是小规模的代码一般不会有这个机会运行这个算法。如果一旦脚本中GC分析算法开始运行了,那么将花费少量的时间节省出来了大量的内存,是一件非常划算的事情。新的GC对一些长期运行的PHP脚本效果更好,比如PHP的DAEMON守护进程,或则PHP-GTK进程等等。

引擎内部GC的实现
  前面已经介绍了新的GC的基本原理以及性能相关的内容,其中一些都是在手册中有简单介绍了,那么这里我们将从源代码的角度来分析一下PHP如何实现新的GC。
1.zval的变化
    在文件Zend/zend_gc.h中,重新定义了分配一个zval结构的宏:
  1. #undef  ALLOC_ZVAL 
  2. #define ALLOC_ZVAL(z)                                   / 
  3.     do {                                                / 
  4.         (z) = (zval*)emalloc(sizeof(zval_gc_info));     / 
  5.         GC_ZVAL_INIT(z);                                / 
  6.     } while (0) 
#undef ALLOC_ZVAL #define ALLOC_ZVAL(z) / do { / (z) = (zval*)emalloc(sizeof(zval_gc_info)); / GC_ZVAL_INIT(z); / } while (0)
关注
打赏
1688896170
查看更多评论

暂无认证

  • 0浏览

    0关注

    107766博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.1183s