继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

PHP5变量的实现

慕村9548890
关注TA
已关注
手记 1298
粉丝 227
获赞 991

概述

PHP 中变量的值存储在 zval 结构体中,变量名存在符号表(symbol table)中,符号表的实际是一个 HashTable

变量的作用域

对于一个请求,任意时刻 PHP 都可以看到两个符号表(symbol_table 和 active_symbol_table),symbol_table 用来维护全局变量,active_symbol_table 是一个指针,指向当前活动的变量符号表,当程序进入到某个函数中时,zend 就会为它分配一个符号表 x 同时将 active_symbol_table 指向 x。通过这样的方式实现全局、局部变量的区分。

zval 结构体

PHP5 中 zval 结构体定义如下:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

zval 结构体中有四个字段:

属性名含义默认值
value存储变量的值
type说明变量的类型
refcount__gc引用计数1
is_ref__gc是否为引用0

zval 整个结构体占用24字节。

value 是个联合体,用于存储不同类型的值:

typedef union _zvalue_value {    long lval;                 // 用于 bool 类型、整型和资源类型
    double dval;               // 用于浮点类型
    struct {                   // 用于字符串
        char *val;        int len;
    } str;
    HashTable *ht;             // 用于数组
    zend_object_value obj;     // 用于对象
    zend_ast *ast;             // 用于常量表达式(PHP5.6 才有)} zvalue_value;

首先我们需要了解联合体的概念,联合体中一次只有一个成员是有效,分配的内存与需要内存最多的成员匹配(需要考虑内存对齐)。在这个联合体中最长的长板是 zend_object_value,需要占用 16 个字节,因此这个联合体最少需要占用 16 字节。

type 的值可以是:IS_NULL、IS_BOOL、IS_LONG、IS_DOUBLE、IS_STRING、IS_ARRAY、IS_OBJECT 和 IS_RESOURCE。

#define IS_NULL     0      /* Doesn't use value */#define IS_LONG     1      /* Uses lval */#define IS_DOUBLE   2      /* Uses dval */#define IS_BOOL     3      /* Uses lval with values 0 and 1 */#define IS_ARRAY    4      /* Uses ht */#define IS_OBJECT   5      /* Uses obj */#define IS_STRING   6      /* Uses str */#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */

引用计数

在 PHP5 中,zval 的内存是单独从堆(heap)中分配的,zval 中 refcount__gc 的值保存 zval 被引用的次数,引用计数如果变成 0,就意味着这个变量已经没用,可以被释放。这里的“引用”不是 PHP 代码中的引用&,而是变量的使用次数。后面两者需要同时出现时会使用“PHP引用”和“引用”来区分两个概念。

写时复制(Copy-On-Write)

对于多个引用来说 zval 只有在没有变化的情况下才是共享的,一旦其中一个引用改变 zval 的值,就需要分离(“separated”)一份 zval,然后修改复制后的 zval。

$a = 43; // $a -> zval_1(type=IS_LONG, value=43, refcount=1)$b = $a; // $a, $b -> zval_1(type=IS_LONG, value=43, refcount=2)$c = $b; // $a, $b, $c -> zval_1(type=IS_LONG, value=43, refcount=3)

// zval 分离$a += 1; // $b, $c -> zval_1(type=IS_LONG, value=43, refcount=2)
         // $a     -> zval_2(type=IS_LONG, value=44, refcount=1)unset($b); // $c -> zval_1(type=IS_LONG, value=43, refcount=1)
           // $a -> zval_2(type=IS_LONG, value=44, refcount=1)unset($c); // zval_1 is destroyed, because refcount=0
           // $a -> zval_2(type=IS_LONG, value=44, refcount=1)
$a = 1;   // $a     -> zval_1(type=IS_LONG, value=1, refcount=1, is_ref=0)$b = &$a; // $a, $b -> zval_1(type=IS_LONG, value=1, refcount=2, is_ref=1)

// zval 分离$c = $a;  // $a, $b -> zval_1(type=IS_LONG, value=1, refcount=2, is_ref=1)
          // $c     -> zval_2(type=IS_LONG, value=1, refcount=1, is_ref=0)

循环引用

我们来看一段代码:

$a = [];        
$a[] = & $a; 
unset($a);

首先创建一个数组 a,a 的 refcount 为 1,将数组的第一个元素按引用指向数组自身这时 a 的 zval 的 refcount 变为2,然后再将数组 unset。这时候变量 a 已经被从符号表中删除了,但 zval 的 refcount 变为 1,并没有被销毁。

垃圾回收机制


为了解决循环引用的问题,PHP 使用了循环回收的方法。当一个 zval 的计数减一时,就有可能属于循环的一部分,这时将 zval 写入到“根缓冲区(gc_root_buffer)”中。当缓冲区满时,潜在的循环会被打上标记并进行回收。

  • 并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后才开始垃圾回收。

  • 可以解决循环引用问题。

  • 可以总将内存泄露保持在一个阈值以下。



作者:某尤
链接:https://www.jianshu.com/p/92356e0ac2c8


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP