PHP内核之zval

原文地址
作者:Twei 主页

前言


之前面试的时候面试官问过php中变量是如何实现的,遗憾的是只答道了大概是用结构体实现的。这篇文章是谷歌之后觉得总结 的比较到位的,故转载进而学习之。

正文


PHP中的数据类型


相对于 C、 C++、 Java等其他编程语言,PHP 是一个弱类型的语言,意味着当我们要使用一个变量时,不需要去声明它的类型。这个特性给我们带来了很多便利,同时有时也会带来一些陷阱。那么,PHP 是真的没有数据类型这个说法吗?

当然不是。在 PHP 官方文档中将 PHP 中的变量划分为三类:标量类型、复杂类型和特殊类型。标量类型包括布尔型(bool)、整型(int)、浮点型(float)和字符串(string);复杂类型包括数组(array)和对象(object);特殊类型包括 NULL 和资源(resource)。所以说 PHP 的变量细分的话,有 8 种数据类型。

总所周知,PHP 的底层是用 C 语言实现的。我们的 PHP 脚本会经过 Zend 引擎解析为 C 代码再执行。那么,一个 PHP 的变量,在 C 语言上是怎么表示的呢?它最终会被解析成什么样呢?

答案就是 zval。 不管什么类型的 PHP 变量,在 PHP 源代码中统一用一个叫做 zval 的结构表示。 zval 可以看做是 PHP 变量在 C 代码中的容器,它存储了这个变量的值、类型等相关信息。

那么我们就看一下 zval 的基本结构(需要一点 C 语言的基本知识)。

zval的基本结构


在 PHP 源代码中 zval 这个结构是一个名叫 _zval_struct 的结构体(struct),具体定义在源代码的 Zend/zend.h 文件中,下面是相关代码的摘录:

struct _zval_struct {
    zvalue_value value;       /* value */ 
    zend_uint refcount__gc;   /* value of ref count */
    zend_uchar type;          /* active type */ 
    zend_uchar is_ref__gc;    /* if it is a ref variable */ 
}; 
typedef struct _zval_struct zval;

也就是说,在 PHP 的源码中,就用这一个结构体表示 PHP 中各种类型的变量,并且还可以实现其他的一些功能,例如垃圾回收(GC:Grabage Collection)。

可以看到它由 4 个字段构成,分别表示这个变量的某个信息。

ZVALUE_VALUE VALUE


value 用来表示变量的实际值,具体来说它是一个 zvalue_value 的联合体(union):

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {                    /* string */
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value,used for array */
    zend_object_value obj;      /* object */
} zvalue_value;

可以看到 _zvalue_value 中只有 5 个字段,但是 PHP 中有 8 种数据类型,那么如何用 5 个字段表示 8 种类型呢?

这算是 PHP 设计比较巧妙的一个地方,它通过复用字段达到了减少字段的目的。例如,在 PHP 内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过 lval 字段存储的;dval 用于存储浮点型;str 存储字符串;ht 存储数组(注意 PHP 中的数组其实是哈希表);而 obj 存储对象类型;如果所有字段全部置为 0 或 NULL则表示 PHP 中的 NULL,这样就达到了用 5 个字段存储 8 种类型的值。

ZEND_UINT REFCOUNT__GC


从它的后缀 gc 可以看到,这个字段是和垃圾回收相关的。

它实际上是一个计数器,用来保存有多少变量指向该zval。在变量生成时,置为1,也就是 refcount = 1。

对变量进行不同的操作会改变它的值。典型的赋值操作如 a= a = b 会使 refcount 加 1,而 unset() 操作会相应的减 1。

通过判断它的值可以进行垃圾回收。在 PHP5.3 之前,使用引用计数的机制来实现 GC:如果一个 zval 的 refcount 减为 0,那么 Zend 引擎会认为没有任何变量指向该 zval,就会释放该 zval 所占的内存空间。但仅仅使用引用计数机制无法释放掉循环引用的 zval,这是就会导致内存泄露(Memory Leak)。

在 5.3 以前,这个字段的名字还叫做 refcount,5.3 以后,在引入新的垃圾回收算法来对付循环引用,作者加入了大量的宏来操作 refcount,为了能让错误更快的显现,所以改名为 refcount__gc, 迫使大家都使用宏来操作 refcount。

类似的, 还有第四个字段 is_ref, 这个值表示了 PHP 中的一个类型是否是引用。
想了解 PHP 的垃圾回收机制,可以参考这篇博客:PHP的垃圾回收机制
注:变量,也可以被称为符号,symbol。所有的符号都存在符号表(symbol table)中, 不同的作用域使用不同的符号表,关于这一点,这篇博客进行了讲解。

ZEND_UCHAR TYPE


这个字段用于表明变量属于 PHP 8 种类型的哪种。在 zend 内部,这些类型对应于下面的宏(代码位置 phpsrc/Zend/zend.h):

#define IS_NULL     0
#define IS_LONG     1
#define IS_DOUBLE   2
#define IS_BOOL     3
#define IS_ARRAY    4
#define IS_OBJECT   5
#define IS_STRING   6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_ARRAY   9
#define IS_CALLABLE 10

ZEND_UCHAR IS_REF__GC


这个字段用于标记变量是否是引用变量。对于普通的变量,该值为 0,而对于引用型的变量,该值为 1。这个变量会影响 zval 的共享、分离等。它也和 PHP 的垃圾回收有关。

PHP7中的zval


上述的 zval 结构,随着时间的发展,暴露出许多问题,例如占用空间大(24 字节)、不支持拓展、 对象和引用效率差等,所以在 PHP7 的时候,对 zval 进行了较大的改变,现在它的结构是这样的:

struct _zval_struct {
    union {
        zend_long         lval;             /* long value */
        double            dval;             /* double value */
        zend_refcounted  *counted;
        zend_string      *str;
        zend_array       *arr;
        zend_object      *obj;
        zend_resource    *res;
        zend_reference   *ref;
        zend_ast_ref     *ast;
        zval             *zv;
        void             *ptr;
        zend_class_entry *ce;
        zend_function    *func;
        struct {
            uint32_t w1;
            uint32_t w2;
        } ww;
    } value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2;
};

虽然看起来变得好大,但其实仔细看,它的字段都是联合体,这个新的 zval 在 64 位环境下,只需要 16 个字节(2 个指针 size)。PHP7 中的 zval,已经变成了一个值指针,它要么保存着原始值,要么保存着指向一个保存原始值的指针。

这部分内容来自鸟哥的GitHub

总结


  1. zval 是一种 C 语言实现的数据结构,功能是作为 PHP 变量的容器;
  2. 它保存了变量的各种信息(如类型和值),并为其他功能(如垃圾回收)提供支持;
  3. 在不同的 PHP 版本中,它的结构不同。PHP7 的 zval 占 16 个字节,PHP5 的要占 24 个字节。

参考


PHP内核探索之变量(1)变量的容器-Zval
PHP垃圾回收深入理解
深入理解PHP7之zval

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值