Skip to the content.

变量

变量的内部实现

变量两个部分组成: 变量名(zval), 变量值(zend_value), PHP 中变量的内存是通过引用计数进程管理的,而且 PHP7 中引用计数是在 zend_value 上,变量之间的赋值,传递通常也是针对 zend_value.

最简单的类型 true, false, long, double, null, 其中true,false,null没有 value,直接根据 type 区分, 而 long,double 的值直接储存在 value 中: zend_long,double, 也就是标量类型不需要额外的 value 指针

字符串

PHP 中字符串通过zend_string表示

分为两类:

数组

array 底层就是普通的有序 HashTable

对象/资源

对象比较常见, 资源指的是 tcp 连接, 文件句柄等等类型

引用

引用是 PHP 中比较特殊的类型, 它实际指向另外一个 PHP 变量, 对它的修改会直接改动实际指向的 zval, 可以简单的理解为 C 中指针

过程为:

产生一个 zend_reference(内嵌 zval) 结构, 这个结构中的 zval 的 value 指向原来的 zval 的 value(如果是布尔,整形,浮点则直接复制原来的值) 将原来的 zval 的类型修改为 IS_REFERENCE 原来的 zval 指向新创建的 zend_reference 结构

简单理解为: 引用时产生zend_reference, 原来变量和现在变量都指向 ref, ref 指向原来的 zend_value, 原来的 zend_value 上的 zend_value->refcount 由 ref->refcount 来负责

$a zval.value.ref zend_reference zend_string $b zval.value.ref

引用只能通过&产生,无法通过赋值传递

PHP 引用只能有一层,不会出现一个引用指向另一个引用的情况

引用计数

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

value 是指针的几种类型才会发生引用计数

不会发生引用计数的几种类型:

会发生引用计数

通过 zval.u1->type_flag 包含IS_TYPE_REFCOUNTED来判断是否支持引用计数

写时复制

引用计数表示多个变量可能指向同一个 value, 然后通过 refcount 统计引用数,如果其中一个变量试图更改 value 的内容则会重新拷贝一份 value 修改,同时断开旧的指向.

只有 string, array 两种类型支持写时复制

通过 zval.u1.type_flag 是否包含 IS_TYPE_COPYABLE 来识别是否可以写时复制

copyable 在以下两种情况下会发生

变量回收

PHP 变量主要包含两种: 主动销毁(unset), 自动销毁(在 return 时减掉局部变量的 refcount; 写时复制断开原来 value 的指向, 这时候会检查断开后旧 value 的 refcount)

垃圾回收

变量回收是根据 refcount 实现的, 但是有些时候变量内部引用了自身, 导致在 unset 变量时, refcount 不能归零, 这种变量就是垃圾

垃圾目前只会出现在 array, object 两种类型中, 所以会针对这两种类型做特殊处理: 当销毁的变量减掉 refcount 后仍然大于零,且类型是 IS_ARRAY IS_OBJECT 则将此 value 放入gc可能垃圾双向列表中,等这个链表达到一定数量后启动检查程序将所有变量检查一遍,如果确定是垃圾则销毁释放

变量是否需要回收通过 u1.type_flag 是否包含 IS_TYPE_COLLECTABLE 来识别

数组

PHP 的数组为什么使用 HashTable 来实现

散列表是根据关键字码(Key value)而直接进行访问的数据结构(key 映射到内存地址上), 通过映射函数直接找值,从而加快查找速度,最理想情况下,无需任何比较就可以找到关键字,查找期望时间 O(1)

数组结构

映射函数原理是什么

arData 散列表和 Bucket 数组一起分配, arData 向后移动到了 Bucket 数组的起始位置,并不是申请内存的起始位置,arData 指针向前移动访问到散列表

arData
索引表 Bucket

arData 前半截为索引表, 后半截为 元素数组表(Buckets)

PHP 使用位运算来建立 key,value 的关系 nIndex = key->h | ht->nTableMask

nTableMask = -nTableSize nTableSize = 2^n |nIndex| <= nTableSize

如何解决 Hash 冲突的

将 Bucket 串成链表, 查找时遍历链表比较 key, PHP 中将链表的指针转化为了数值指向, 即: 指向冲突元素的指针保存到了 value 的zval中.

扩容

重建散列表

当删除元素到达阈值或扩容后都需要重建散列表

因为 value 在 Bucket 位置移动了或哈希数组 nTableSize 变化导致 key 与 value 的映射关系改变,重建过程实际就是遍历 Bucket 数组中的 value, 然后重新计算映射值更新到散列表, 移除已删除的 value

静态变量

静态变量通过 static 关键词创建, 当程序执行离开函数域时静态变量的值被保留下来,下次执行时仍然可以使用之前的值

保存在 zend_op_array->static_variables 中, 这是一个哈希表

静态变量只会初始化一次, 发生在编译阶段而不是执行阶段

static $count = 1 实际上可以理解为 $count = &static_variables["count"] , 也就是说 $count 实际上是一个局部变量

静态变量赋值过程可以理解为: 根据变量名从 static_variables 总取出对应的 zval, 然后将它修改为引用类型并赋值给局部变量.

全局变量

在函数,类外定义的变量称为全局变量, 可以在成员函数, 成员方法中通过 global 关键字引入使用

全局变量在整个请求执行期间始终存在

全局变量的访问

全局变量是将原来的值转换为引用, 在 global 导入的作用域内创建一个局部变量指向该引用.

超全局变量

PHP 内核中定义了一些全局变量, 不需要 global 关键字引入: $GLOBALS, $_SERVER, $_REQUEST, $_POST, $_GET, $_FILE, $_ENV, $_COOKIE, $_SESSION, argv, argc

销毁

全局变量只有在请求结束时才会销毁

常量

常量由define('NAME', 1234)来定义, 默认大小写敏感

在内核中常量储存在EG(zend_constants)哈希表中, 也是根据常量名直接在哈希表中查找

常量数据结构:

销毁

非持久化常量将会在 request 结束时销毁,倒序遍历常量哈希表,依次销毁, 直到遇到持久化常量,通常来说, 持久化常量会定义在非持久化常量之前 持久化常量将会在php_module_shutdown时销毁