PHP变量和内存管理

zval

PHP是弱类型的语言,声明变量不需要指定类型,PHP是怎么做到的呢?

首先,所有变量都是用一个叫做zval的变量容器来保存的,结构如下:

typedef struct _zval_struct {
    zvalue_value value; # 变量具体的值
    zend_uint refcount; # 引用计数
    zend_uchar type; # 变量类型
    zend_uchar is_ref; # 是否引用
} zval;

然后value是一个联合体:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

当我们给变量赋值的时候,PHP自动识别变量类型并保存到type中,然后下一次通过type来决定获取变量的方式。

但是这个zval结构并没有保存变量名称,PHP是怎么找到对应的变量的呢?

当我们创建变量的时候,php会把变量内容保存到zval,然后把变量名称和指向zval的指针保存到一个叫做符合表的东西里面,因此,PHP就可以通过变量名找到对应的zval来获取到变量值

php-zval1

那么is_ref和refcount又是干嘛的?考虑下面的代码:

$a = 'hello world';
$b = $a;

1.首先申请一块内存把变量内容保存到一个zval结构体中,然后把变量名和变量内容的映射关系保存到符合表中

2.复制变量a给变量b,这时候如果php再申请一块内存保存同样的内容到一个新的zval中,有点浪费了,所以就有了引用

那么使用引用以后,第2步是怎么处理的呢?

只需要把b保存到符号表中并且指向和a指向的同一个zval就行了,这样a和b都指向了同一个zval,然后这个zval的refcount加1,表示同时有几个变量在用到它

php-zval2

$a = 'hello world';
$b = $a;
unset($a);

php-zval3

$a = 'hello world';
$b = $a;
$a = 1;

php-zval4

$a = 'hello world';
$b = &$a;

php-zval5

$a = 'hello world';
$b = &$a;
$a = 1;

php-zval6

$a = 'hello world';
$b = $a;
$c = &$a;

php-zval7

垃圾回收

通常,我们写的代码,有两个机会来做到内存的释放:

  1. 首先,如果一个变量的引用计数变成0(通常是调用了unset),它所对应的zval将会直接释放(可能不是真的删除,只是释放掉符号表与zval的映射关系)
  2. 其次,在脚本执行完后PHP会释放掉。

如上所述,在PHP5.3之前,没有专门的垃圾回收,zval内存的释放只是简单地通过引用计数是否为0来进行。通过引用计数为0来销毁变量会有个问题,当数组或者对象有循环引用的情况下,当unset的时候zval refcount并没有变成0,这一部分的内存是没有办法释放掉的,会造成内存泄漏,虽然脚本执行完后PHP会释放,但是当PHP作为守护进程需要长时间运行,或者在运行过程中占用内存比较大的时候,这种机制不能有效地进行垃圾回收,造成内存占用过大。这个时候可以调用gc_collect_cycles来做到强制回收。

在PHP5.3之后,对垃圾回收有了改进,当引用计数变为0时,直接清除。当引用计数减1以后大于0时,认为其可能是垃圾,把它放入垃圾缓冲区,当缓冲区满了(默认是1w个zval)才统一进行垃圾回收。这里需要注意的是:只有减掉以后引用计数仍然大于0的zval才会被认为是垃圾,所以如果在代码中不调用unset是不会减掉引用计数的(除了变量分离的情况),那自然也就不会进行垃圾回收,只有等脚本结束才会被释放掉。执行垃圾回收的时候,PHP遍历zval的每个元素进行模拟删除,如果zval的引用计数变成0了,证明此zval是个垃圾。

参考资料

  1. 深入理解PHP原理之变量
  2. php manual-回收周期
  3. 新的垃圾回收