1. 变量

1.1. 变量的内部实现

变量两个部分组成: 变量名(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
    • u2 利用空余空间储存一些辅助值
  • zend_value
    • zend_{type} 变量类型
      • zend_long 直接储存
      • double 直接储存
      • zend_string 指针
      • zend_array 指针
      • zend_object 指针
      • zend_resource 指针
      • zend_reference 引用类型, 通过 &$var_name 定义的
    • zend_refcounted 引用数量

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

1.1.1. 字符串

PHP 中字符串通过zend_string表示

分为两类:

  • IS_STR_PERSISTENT (通过 malloc 分配的)
  • IS_STR_INTERNED (php 代码里写的一些字面量, 比如函数名,变量值)
  • IS_STR_PERMANENT (永久值,生命周期大于 request)
  • IS_STR_CONSTANT (常量)
  • IS_STR_CONSTANT_UNQUALIFIED (通过 flag 保存)

1.1.2. 数组

array 底层就是普通的有序 HashTable

1.1.3. 对象/资源

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

1.1.4. 引用

引用是 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 引用只能有一层,不会出现一个引用指向另一个引用的情况

1.1.5. 引用计数

引用计数是指在 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来判断是否支持引用计数

1.1.6. 写时复制

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

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

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

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

  • 从 literal 变量区复制到局部变量区
  • 局部变量分离时(写时复制), 如果改变变量内容引用计数大于 1 则需要分离

1.1.7. 变量回收

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

1.1.8. 垃圾回收

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

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

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

1.2. 数组

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中.

1.2.1. 扩容

  • PHP 散列表的大小为2^n,插入时如果容量不够, 先会判断已删除元素的比例, 如果到达阈值(nNumUsed-nNumOfElements>nNumOfElements>>5),则将已删除元素移除, 重建索引, 如果未达到阈值则进行扩容操作,扩大为当前大小的 2 倍,将当前 Bucket 数组复制到新的空间,然后重建索引.

1.2.2. 重建散列表

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

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

1.3. 静态变量

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

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

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

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

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

1.4. 全局变量

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

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

全局变量的访问

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

1.4.1. 超全局变量

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

1.4.2. 销毁

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

1.5. 常量

常量由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时销毁

results matching ""

    No results matching ""