变量
变量的内部实现
变量两个部分组成: 变量名(zval), 变量值(zend_value), PHP 中变量的内存是通过引用计数进程管理的,而且 PHP7 中引用计数是在 zend_value 上,变量之间的赋值,传递通常也是针对 zend_value.
- zval
- zend_value 指向值
- u1 变量类型储存
- type
- IS_UNDEF
- IS_NULL
- IS_FALSE
- IS_TRUE
- IS_LONG
- IS_DOUBLE
- IS_STRING
- IS_ARRAY
- IS_OBJECT
- IS_RESOURCE
- IS_REFERENCE
- IS_CONSTANT
- IS_CONSTANT_AST
- _IS_BOOL
- IS_CALLABLE
- IS_INDIRECT
- IS_PTR
- type
- u2 利用空余空间储存一些辅助值
- zend_value
- zend_{type} 变量类型
- zend_long 直接储存
- double 直接储存
- zend_string 指针
- zend_array 指针
- zend_object 指针
- zend_resource 指针
- zend_reference 引用类型, 通过 &$var_name 定义的
- zend_refcounted 引用数量
- zend_{type} 变量类型
最简单的类型 true
, false
, long
, double
, null
, 其中true
,false
,null
没有 value,直接根据 type 区分, 而 long,double 的值直接储存在 value 中: zend_long,double, 也就是标量类型不需要额外的 value 指针
字符串
PHP 中字符串通过zend_string
表示
分为两类:
- IS_STR_PERSISTENT (通过 malloc 分配的)
- IS_STR_INTERNED (php 代码里写的一些字面量, 比如函数名,变量值)
- IS_STR_PERMANENT (永久值,生命周期大于 request)
- IS_STR_CONSTANT (常量)
- IS_STR_CONSTANT_UNQUALIFIED (通过 flag 保存)
数组
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 是指针的几种类型才会发生引用计数
不会发生引用计数的几种类型:
- true/false/double/long/null
- interned string(内部字符串, 所有字符都可以认为是这种类型, function name, class name, variable name, 静态字符串等, 比如
$a = "hi"
, 后面字符串不变,生命周期为 request 期间,完成后会统一销毁释放,无需在运行期间通过引用计数管理内存) - immutable array(只有在用 opcache 的时候才会用到这种类型)
会发生引用计数
- string
- array
- object
- resource
- reference
通过 zval.u1->type_flag 包含IS_TYPE_REFCOUNTED
来判断是否支持引用计数
写时复制
引用计数表示多个变量可能指向同一个 value, 然后通过 refcount 统计引用数,如果其中一个变量试图更改 value 的内容则会重新拷贝一份 value 修改,同时断开旧的指向.
只有 string, array 两种类型支持写时复制
通过 zval.u1.type_flag 是否包含 IS_TYPE_COPYABLE 来识别是否可以写时复制
copyable 在以下两种情况下会发生
- 从 literal 变量区复制到局部变量区
- 局部变量分离时(写时复制), 如果改变变量内容引用计数大于 1 则需要分离
变量回收
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)
数组结构
- _zend_array
- Bucket *arData 指向储存元素数组的第一个 Bucket, 插入元素时按顺序依次插入数组 保证了有序性
- nNumUsed 已用 Bucket 数, 当一个元素从数组中删除时, Bucket 并不会移除,而是将 zval 修改为 IS_UNDEF, 只有扩容时发现相差达到
nNumUsed-nNumOfElements > nNumOfElements >> 5
这个数量时,才会将已删除的元素全部移除,重新构建哈希表 - nNumOfElements 哈希表有效元素
- nTableSize 哈希表有效元素
- nNextFreeElement 下一个可用的数值索引
映射函数原理是什么
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
中.
扩容
- PHP 散列表的大小为
2^n
,插入时如果容量不够, 先会判断已删除元素的比例, 如果到达阈值(nNumUsed-nNumOfElements>nNumOfElements»5),则将已删除元素移除, 重建索引, 如果未达到阈值则进行扩容操作,扩大为当前大小的 2 倍,将当前 Bucket 数组复制到新的空间,然后重建索引.
重建散列表
当删除元素到达阈值或扩容后都需要重建散列表
因为 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)
哈希表中, 也是根据常量名直接在哈希表中查找
常量数据结构:
- zend_constant
- zval 常量值
- zend_string 常量名
- flags 常量标识位 [CONST_CS 大小写敏感][const_persistent 持久化的][CONST_CT_SUBST 允许编译时替换]
- module_number 所属扩展
销毁
非持久化常量将会在 request 结束时销毁,倒序遍历常量哈希表,依次销毁, 直到遇到持久化常量,通常来说, 持久化常量会定义在非持久化常量之前
持久化常量将会在php_module_shutdown
时销毁