PHP

深入了解PHP的Zval - PHP内核的关键概念

silverwq
2022-09-29 / 0 评论 / 311 阅读 / 正在检测是否收录...

概述

变量是一个语言实现的基础,变量有两个组成部分:变量名、变量值,PHP中可以将其对应为:zval、zend_value,这两个概念一定要区分开,PHP中变量的内存是通过引用计数进行管理的,而且PHP7中引用计数是在zend_value而不是zval上,变量之间的传递、赋值通常也是针对zend_value。

php7的变量的基础结构:

// zend_value结构体,保存具体变量类型的值或指针
typedef union _zend_value {
    zend_long         lval;    //int整形
    double            dval;    //浮点型
    zend_refcounted  *counted;
    zend_string      *str;     //string字符串
    zend_array       *arr;     //array数组
    zend_object      *obj;     //object对象
    zend_resource    *res;     //resource资源类型
    zend_reference   *ref;     //引用类型,通过&$var_name定义的
    zend_ast_ref     *ast;     //下面几个都是内核使用的value
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

//zend_types.h zval结构体
typedef struct _zval_struct     zval;
struct _zval_struct {
    zend_value        value; //变量实际的value
    // u1 主要是用于保存变量的类型
    union {
        struct {
            ZEND_ENDIAN_LOHI_4( //这个是为了兼容大小字节序,小字节序就是下面的顺序,大字节序则下面4个顺序翻转
                zend_uchar    type,  //变量类型
                zend_uchar    type_flags,  //类型掩码,不同的类型会有不同的几种属性,内存管理会用到
                zend_uchar    const_flags,
                zend_uchar    reserved)     //call info,zend执行流程会用到
        } v;
        uint32_t type_info; //上面4个值的组合值,可以直接根据type_info取到4个对应位置的值
    } u1;
    // 这个值纯粹是个辅助值
    union {
        uint32_t     var_flags;
        uint32_t     next;                 //哈希表中解决哈希冲突时用到
        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的结构体由,一个union类型的zend_value和两个union:u1、u2组成,他们的大小为16个字节。

  1. zend_value根据类型存储各自的值或者指针,对于整型和浮点型这种简单的数值字节存储,否则存的是值的指针,占用8个字节;
  2. u1是用于存储变量的类型,变量的类型就通过u1.v.type区分,其它几个应该都是变量的类型,只是不同场景用不同的值。
  3. u2是一个扩展的值。

zvalue结构体

这里举例几个了解下就好。
zvalue标量类型存储
最简单的类型是true、false、long、double、null,其中true、false、null没有value,直接根据type区分,而long、double的值则直接存在value中:zend_long、double,也就是标量类型不需要额外的value指针。
zvalue字符串类型存储
通过指针指向字符串结构体

struct _zend_string {
    zend_refcounted_h gc;// 变量引用信息,比如当前value的引用数,所有用到引用计数的变量类型都会有这个结构
    zend_ulong        h; // 哈希值,数组中计算索引时会用到
    size_t            len; //字符串长度,通过这个值保证二进制安全
    char              val[1];//字符串内容,变长struct,分配时按len长度申请内存
};

引用类型
&首先会创建一个zend_reference结构,其内嵌了一个zval,这个zval的value指向原来zval的value(如果是布尔、整形、浮点则直接复制原来的值),然后将原zval的类型修改为IS_REFERENCE,原zval的value指向新创建的zend_reference结构。

struct _zend_reference {
    zend_refcounted_h gc;// 引用计数
    zval              val;//
};

例如:

$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)

// 通过以下函数可以调试
xdebug_debug_zval('a');
xdebug_debug_zval('b');

最终的结果如图:
l8mcilfn.png
注意:引用只能通过&产生,无法通过赋值传递,比如:

$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = $b;                    //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2)
                            //$c    ->   

$b = &$a这时候$a、$b的类型是引用,但是$c = $b并不会直接将$b赋值给$c,而是把$b实际指向的zval赋值给$c。
这个也表示PHP中的 引用只可能有一层 ,不会出现一个引用指向另外一个引用的情况 ,也就是没有C语言中指针的指针的概念

内存管理

因为如果对变量都是做拷贝的话,对于字符串、数组、对象等结构,对性能的开销很大,所以php里的方案是:引用计数+写时复制。

引用计数

引用计数是指在value中增加一个字段refcount记录指向当前value的数量,变量复制、函数传参时并不直接硬拷贝一份value数据,而是将refcount++,变量销毁时将refcount--,等到refcount减为0时表示已经没有变量引用这个value,将它销毁即可。

$a = "time:" . time();   //$a       ->  zend_string_1(refcount=1)
$b = $a;                 //$a,$b    ->  zend_string_1(refcount=2)
$c = $b;                 //$a,$b,$c ->  zend_string_1(refcount=3)

unset($b);               //$b = IS_UNDEF  $a,$c ->  zend_string_1(refcount=2)

例外:

$a = "hi~";
$b = $a;
// $a,$b -> zend_string_1(refcount=0,val="hi~"),引用计数却是0

事实上并不是所有的PHP变量都会用到引用计数,标量:true/false/double/long/null是硬拷贝自然不需要这种机制,但是除了这几个还有两个特殊的类型也不会用到:

  1. interned string,内部字符串,我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:$a = "hi~";后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:char *a = "hi~";,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。
  2. immutable array,只有在用opcache的时候才会用到这种类型,不清楚具体实现,暂时忽略。

写时复制

多个变量可能指向同一个value,然后通过refcount统计引用数,这时候如果其中一个变量试图更改value的内容则会重新拷贝一份value修改,同时断开旧的指向,写时复制的机制在计算机系统中有非常广的应用,它只有在必要的时候(写)才会发生硬拷贝,可以很好的提高效率,下面从示例看下:

$a = array(1,2);
$b = &$a;
$c = $a;

//发生分离
$b[] = 3;

最终的结果:
l8mdiatp.png

不是所有类型都可以copy的,比如对象、资源,事实上只有string、array两种支持,与引用计数相同,也是通过zval.u1.type_flag标识value是否可复制的,也就是说变量有个copyable的属性。

变量回收

PHP变量的回收主要有两种:主动销毁、自动销毁。

  1. 主动销毁指的就是 unset
  2. 而自动销毁就是PHP的自动管理机制,在return时减掉局部变量的refcount,即使没有显式的return,PHP也会自动给加上这个操作,另外一个就是写时复制时会断开原来value的指向,这时候也会检查断开后旧value的refcount。

垃圾回收

PHP变量的回收是根据refcount实现的,当unset、return时会将变量的引用计数减掉,如果refcount减到0则直接释放value,这是变量的简单gc过程,但是实际过程中出现gc无法回收导致内存泄漏的bug,先看下一个例子:

$a = [1];
$a[] = &$a;

unset($a);

unset($a)之前引用关系:
![l8mdvidm.png](https://www.xiaoqiuyinboke.cn/usr/uploads/2022/09/107427356.png)
unset($a)之后
l8mdvup9.png
可以看到,unset($a)之后由于数组中有子元素指向$a,所以refcount > 0,无法通过简单的gc机制回收,这种变量就是垃圾,垃圾回收器要处理的就是这种情况,目前垃圾只会出现在array、object两种类型中,所以只会针对这两种情况作特殊处理:当销毁一个变量时,如果发现减掉refcount后仍然大于0,且类型是IS_ARRAY、IS_OBJECT则将此value放入gc可能垃圾双向链表中,等这个链表达到一定数量后启动检查程序将所有变量检查一遍,如果确定是垃圾则销毁释放。

参考

https://github.com/pangudashu/php7-internal/blob/master/2/zval.md

0

评论 (0)

取消