变量的类型和实现
PHP 的变量是弱类型的,也实现了如整型、浮点型、字符串、数组和对象等类型。PHP 中的变量是使用结构体 zval
来表示的,在介绍 PHP 7 的 zval
之前,先了解一下 PHP 5 的 zval
设计。
PHP 5 的 zval
首先来看 PHP 5 中 _zval_struct(zval)
这个结构体:
struct _zval_struct { // zval
/* 变量信息 */
zvalue_value value; /* value值 */
zend_uint refcount__gc;
zend_uchar type; /* 类型 */
zend_uchar is_ref__gc;
};
typedef union _zvalue_value {
long lval; /* 长整型 */
double dval; /* 浮点型 */
struct {
char *val;
int len;
} str; /* String value */
HashTable *ht; /* Array or object (hash table) */
zend_object_value obj; /* Object value */
zend_ast *ast; /* zend_ast 结构体用于表示抽象语法树中的节点 */
} zvalue_value;
PHP 5 的 zval
核心由一个 zvalue_value
类型的联合体和 zend_uchar
类型的 type
组成。在 PHP 5.3 之后相继引入了。
-
refcount__gc
字段通过引用计数进行垃圾回收(PHP 使用引用计数来管理变量的生命周期。当引用计数变为零时,变量的内存会被释放。)。 -
type:type 字段表示变量的类型。常见的类型有:
-
IS_NULL (0)
-
IS_LONG (1)
-
IS_DOUBLE (2)
-
IS_BOOL (3)
-
IS_ARRAY (4)
-
IS_OBJECT (5)
-
IS_STRING (6)
-
IS_RESOURCE (7)
-
IS_CONSTANT (8)
-
IS_CONSTANT_AST (9)
-
-
同时增加了新的字段
is_ref__gc
来标记是否为引用类型(如果是引用,则该值为 1,否则为 0。)。
默认在 i386:x86-64
架构下,上面的 zvalue_value
结构体中 lval
和 dval
大小为 8
字节,str
结构体大小为 12
字节,ht
和 ast
是指针类型,大小为 8
字节,obj
结构体大小为 12
字节,所以在内存对齐的情况下 _zval_struct
中的 value
大小为 16
字节,加上 refcount__gc
大小为 4
字节和两个 1
字节的 type
、is_ref__gc
, _zval_struct
结构体本身大小为 24
字节(考虑到结构体对齐)。根据 3.1.2 节中讨论的结构体和联合体的知识,PHP 5 中 zval
的示例如图3-4所示。

假设有一个 PHP 变量:
在 PHP 内部,它可能会被表示为一个
当我们执行
引用计数和垃圾回收 PHP 5 中的垃圾回收主要通过引用计数来管理。当变量的引用计数减少到零时,变量的内存会被释放。例如:
在这个例子中, |
这是不是说,PHP 5 中一个变量就占用一个 zval
呢?其实不是的。PHP 5 中现有的 zval
结构里每个字段都有明确的定义,不可轻易修改,因此在 PHP 5 时代一些对 zval
的优化都是通过结构映射的方式,例如在 PHP 5.3 之后为了解决循环引用的问题,通过重写分配 zval
的宏,对 zval
进行扩充。新的分配方法如下所示:
#undef ALLOC_ZVAL
#define ALLOC_ZVAL(z) \
do { \
(z) = (zval*)emalloc(sizeof(zval_gc_info)); \
GC_ZVAL_INIT(z); \
} while (0)
#define GC_ZVAL_INIT(z) { \
(z)->refcount__gc = 1; \
(z)->type = IS_NULL; \
(z)->is_ref__gc = 0; \
}
-
设置引用计数:将
refcount__gc
设置为 1,表示这个zval
当前有一个引用。 -
设置类型:将
type
设置为IS_NULL
,表示这个zval
目前是一个空值。 -
设置引用标识:将
is_ref__gc
设置为 0,表示这个zval
目前不是一个引用。
|
这里申请的结构体为 _zval_gc_info
,是由一个 zval
结构体和一个 u
联合体组成的,其中 u
大小为 8
字节,_zval_gc_info
结构体如下所示:
typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
通过这种方式,PHP 5 中一个变量实际分配了 32
字节,如图3-5所示。

扩充后的 zval
带来了新的问题,因为整型和浮点型不需要进行 gc
,所以对于整型和浮点型会有内存浪费。
循环引用是指两个或多个对象相互引用对方,导致引用计数无法减少到零,进而导致内存泄漏。整型和浮点型变量无法形成循环引用,因为它们的值是原始类型,没有指向其他变量的引用。 整型和浮点型变量的内存管理非常简单。它们的值直接存储在
对于整型变量, |
不仅如此,在开启 Zend
内存池的情况下,zval_gc_info
在内存池中分配,内存池会为每个 zval_gc_info
额外申请一个大小为 16
字节的 zend_mm_block
结构体(限于篇幅,这里不展开讨论),用来存放内存相关信息。zend_mm_block
结构如下(size_t
占用 8
字节):
typedef struct _zend_mm_block_info {
size_t _size;
size_t _prev;
} zend_mm_block_info;
typedef struct _zend_mm_block {
zend_mm_block_info info;
} zend_mm_block;
最终一个变量在 PHP 5 中实际占用的内存大小为 48
字节,内存占用情况如图3-6所示。

这 48
字节的大小其实有很多的浪费,而这点 PHP 开发者在 PHP 7 中做了重点优化,那么接下来看一下 PHP 7 中 zval
的实现。
PHP 7 的 zval
PHP 7 中 zval
结构体有了一些变化,zval
依然保留了 value
字段,但跟 PHP 5 不同的是 value
里面支持更丰富的类型,且 PHP 7 的 zval
不再存储复杂类型的结构,复杂类型的数据都是通过指针操作的,新的联合体中 value
的内存占用只有 8
字节。zval
结构体如下所示:
struct _zval_struct {
zend_value value; /* 变量的值 */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* 标明zval类型,即变量类型(如 IS_NULL、IS_LONG、IS_DOUBLE、IS_STRING、IS_ARRAY、IS_OBJECT 等) */
zend_uchar type_flags, /* 用于存储变量类型的额外标志信息。 */
zend_uchar const_flags, /* 用于常量的标志信息。 */
zend_uchar reserved) /* 保留字段,用于未来的扩展。 */
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* 用来解决哈希冲突,详见第5章*/
uint32_t cache_slot; /* 运行时缓存 */
uint32_t lineno; /* 对于zend_ast_zval存行号 */
uint32_t num_args; /* EX(This) 参数个数*/
uint32_t fe_pos; /* foreach的位置 */
uint32_t fe_iter_idx; /* foreach游标的标记*/
uint32_t access_flags; /* 类的常量访问标识 */
uint32_t property_guard; /* 单一属性保护 */
} u2;
};
保留的 value
字段,它在 PHP 7 中的定义如下所示:
typedef union _zend_value {
zend_long lval; /* 整型 */
double dval; /* 浮点型 */
zend_refcounted *counted; /*引用计数*/
zend_string *str; /*字符串类型*/
zend_array *arr; /*数组类型*/
zend_object *obj; /*对象类型*/
zend_resource *res; /*资源类型*/
zend_reference *ref; /*引用类型*/
zend_ast_ref *ast; /*抽象语法树*/
zval *zv; /*zval类型*/
void *ptr; /*指针类型*/
zend_class_entry *ce; /*class类型*/
zend_function *func; /*function类型*/
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
|
从上面的定义可以看出,value
支持更多的类型。除了 value
字段之外,zval
结构体中还有两个重要的字段 u1
和 u2
,它们都是联合体结构,却各有用途,用于优化内存布局和性能。
u1 中 v 值中的字段含义
-
type
:记录变量类型。 -
type_flag
:对应变量类型特有的标记,不同类型的变量对应的flag
也不同。所对应的标记如下:/* zval.u1.v.type_flags */ IS_TYPE_CONSTANT //是常量类型 IS_TYPE_IMMUTABLE //不可变的类型,比如存在共享内存中的数组 IS_TYPE_REFCOUNTED //需要引用计数的类型,表示变量是有引用计数的。大多数 PHP 变量都是有引用计数的,这使得 PHP 可以使用引用计数技术来自动管理内存。 IS_TYPE_COLLECTABLE //可能包含循环引用的类型(IS_ARRAY, IS_OBJECT) IS_TYPE_COPYABLE //可被复制的类型
例如,如果一个
zval
结构体表示一个常量,并且这个常量是不可变的,它的type_flags
可能会设置为IS_TYPE_CONSTANT | IS_TYPE_IMMUTABLE
。type_flags
的具体使用取决于 PHP 引擎内部的实现和需求。它通过在zval
结构体中提供额外的位掩码信息,允许 PHP 引擎在运行时更细致地处理不同类型的变量和常量。这种灵活性和细节化的标志信息有助于 PHP 在处理各种情况下能够精确控制变量的行为和属性。 -
const_flag
:常量类型的标记,对应的属性有:/* zval.u1.v.const_flags */ #define IS_CONSTANT_UNQUALIFIED 0x010 #define IS_CONSTANT_VISITED_MARK 0x020 #define IS_CONSTANT_CLASS 0x080 /* __CLASS__ trail类 */ #define IS_CONSTANT_IN_NAMESPACE 0x100 /* 只用在opline->extended_value */
-
reserved
:保留字段。
PHP 7 的 zval
类型字段改为使用联合体结构中的 u1.v.type
表示,u1
由 type_info
和结构体 v
组成,由 3.1.2 节中联合体的知识可知道,v
和 type_info
共享一块内存,两个字段长度均为 4 字节,修改 type_info
等同于修改结构体 v
中的值,其实 type_info
就是 v
中的 4 个 char
的组合,如图3-7所示。

以字符串为例,字段 u1.v.type
值为 6(IS_STRING
,3.2.3节会详细讨论 PHP 7 中的变量类型),同时字符串又是可以引用和可拷贝的,因此 u1.v.type_flag
值为 24(S_TYPE_COPYABLE | IS_TYPE_REFCOUNTED
),这样 u1.type_info
值为 6150。
在 PHP 7 开始的版本通过 value
和 u1
已经可以表示任何类型,并记录一些类型的属性。另外还有一个 u2
,其实是后来增加的辅助字段,u2
里面的字段均是 uint32_t
类型,占用 4 字节,所以 u2
的大小是 4 字节,这样 zval
总共占用的内存是 16 字节,但是如果没有 u2
字段,在内存对齐的情况下,zval
同样占用 16 字节。与其浪费,不如用来记录一些特殊信息。那么 u2
都记录了哪些信息呢?
u2 中的字段信息
-
next
:用来解决哈希冲突问题,记录冲突的下一个元素位置,具体会在第 5 章中详细说明。 -
cache_slot
:运行时缓存。在执行函数时会优先去缓存中查找,若缓存中没有,会在全局的 function 表中查找。 -
lineno
:文件执行的行号,应用在 AST 节点上。Zend 引擎在词法和语法解析时会将当前文件的行号记录下来,记录在zend_ast
中的 lineno 中,如果zend_ast
这个节点的 kind 刚好是 ZEND_AST_ZVAL(值为 64),则会将该zend_ast
强制转换成zend_ast_zval
类型,而对应的 lineno 则记录在zend_ast_zval
结构体中内嵌的zval
里。这部分会在第 11 章中详细展开。 -
num_args
:函数调用时传入参数的个数。 -
fe_pos
:遍历数组时的当前位置。比如在对数组执行foreach
时,fe_pos
每执行一次都会加 1。当再次调用foreach
对数组遍历时,会首先对数组的fe_pos
指针重置。这同样也会在第 5 章中详细说明。 -
fe_iter_idx
:跟 fe_pos 用途类似,只是这个字段是针对对象的。对象的属性也是 HashTable,传入的参数是对象时,会获取对象的属性表,因此遍历对象就是遍历对象的属性。 -
access_flags
:对象类的访问标志,常用的标识有public
、protected
、private
。这个会在第 6 章中阐述。 -
property_guard
:防止类中魔术方法的循环调用,比如__get
、__set
等。
通过上面的介绍发现,u2
的辅助字段里面记录了很多类型的信息,这些信息对内部功能的实现都有很大好处,或提升了缓存友好性或减少了内存寻址的操作。同时相对于 PHP 5 时代的 zval
, PHP 7 的 zval 节省了很大的内存。PHP 7 的 zval
内存占用如图3-8所示。

在 PHP 5 时代,所有的变量都在 堆 中申请,但是对于临时变量是没有必要的,而 PHP 7 对此做了优化,这种临时变量直接在 栈 中申请。接下来先讨论一下 PHP 7 的变量类型。
PHP 7 变量类型
PHP 7 中变量的类型定义在 zend_types.h
文件中,不仅有常见的类型,还有一些只在内部使用的类型,具体定义如下:
#define IS_UNDEF 0 /*标记未使用类型*/
#define IS_NULL 1 /*NULL*/
#define IS_FALSE 2 /*布尔false*/
#define IS_TRUE 3 /*布尔true*/
#define IS_LONG 4 /*长整型*/
#define IS_DOUBLE 5 /*浮点型*/
#define IS_STRING 6 /*字符串*/
#define IS_ARRAY 7 /*数组*/
#define IS_OBJECT 8 /*对象*/
#define IS_RESOURCE 9 /*资源类型*/
#define IS_REFERENCE 10 /*参考类型(内部使用)*/
#define IS_CONSTANT 11 /*常量类型*/
#define IS_CONSTANT_AST 12 /*常量类型的AST树*/
/*伪类型*/
#define _IS_BOOL 13 /*表示变量的类型是布尔类型*/
#define IS_CALLABLE 14 /*是一个类型常量,用于判断一个变量是否可以作为可调用的函数或方法*/
#define IS_ITERABLE 19 /*是 PHP 7.1 引入的一个类型常量,用于判断一个变量是否可以迭代*/
#define IS_VOID 18 /*在 PHP 7.1 引入了 void 类型的返回声明,用于函数声明时指定函数没有返回值*/
/*内部类型*/
#define IS_INDIRECT 15 /*间接类型*/
#define IS_PTR 17 /*指针类型*/
#define _IS_ERROR 20 /*错误类型*/
PHP 7 中定义了 20 种宏,用来标记 u1.v.type
字段,其中伪类型将逐渐淘汰,这里暂不讨论。根据不同的 type
值取 value
中对应的不同值。以 u1.v.type
值为 IS_ARRAY
为例,那么取 value.arr
的值,对应 zend_array
。同样,如果 u1.v.type
值为 IS_LONG
,通过 value.lval
取值。
除了常见类型之外,这里有几个新增的类型需要注意。
-
IS_UNDEF
:标记未定义,表示数据可以被覆盖或者删除。比如在对数组元素进行unset
操作时,PHP 7 并不会直接将数据从分配给HashTable
的内存中删掉,而是先将该元素所在的Bucket
的位置标记为IS_UNDEF
,当HashTable
中IS_UNDEF
元素个数到达一定阈值时,进行rehash
操作时再将IS_UNDEF
标记的元素覆盖或删除。 -
IS_TRUE
和IS_FALSE
:这里将IS_BOOL
优化成两个,直接将布尔类型的标记记录在type
中,这样做可以优化类型的检查,不需要再做一次类型判断。 -
IS_REFERENCE
:是新增的类型,PHP 7 中使用不同的处理方式来处理 “&”,后面展开阐述。 -
IS_INDIRECT
:同样也是新增的类型,由于 PHP 7 中HashTable
的设计跟 PHP 5 中有很大的不同,所以在解决全局符号表访问CV
变量表的问题上,引入了IS_INDRECT
类型。 -
IS_PTR
:该类型被定义为指针类型,用来解析value.ptr
,通常用在函数类型上。比如声明一个函数或者方法。 -
_IS_ERROR
:为新增的错误类型,校验zval
的类型是否合法。
介绍完 PHP 7 中变量的类型,下面来对每种类型进行详细的探讨。
整型和浮点型
对于整型和浮点型的数据,由于其占用空间小,在 zval
中是 直接存储 的,所以在进行赋值的时候会直接创建两个 zval
,在对应的 value
中取 lval
或 dval
。举例如下:
$a = 10; // $a = zval_1(u1.v.type=IS_LONG, value.lval=10)
$b = $a; // $a = zval_1(u1.v.type=IS_LONG, value.lval=10)
// $b = zval_2(u1.v.type=IS_LONG, value.lval=10)
$a = 20; // $a = zval_1(u1.v.type=IS_LONG, value.lval=20)
// $b = zval_2(u1.v.type=IS_LONG, value.lval=10)
unset($a); // $a = zval_1(u1.v.type=IS_UNDEF, value.lval=20)
// $b = zval_2(u1.v.type=IS_LONG, value.lval=10)
对于上面的代码,详细说明如下。
-
对于
$a=10
,是一个整型变量,将生成一个zval
,其中u1.v.type=IS_LONG
,整型value.lval=10
。 -
对于
$b = $a
,此时,直接拷贝了一个zval
,因为zval
只有 16 字节,所以这里没有做写时拷贝(copy-on-write),而是直接做了拷贝。 -
对于
$a=20
,此时,修改了a
对应的value.lval=20
,而b
对应的value.lval
并不改变。 -
对于
unset($a)
,此时,修改a
的u1.v.type=IS_UNDEF
,但此时并不是真正把内存释放,因为b
是拷贝出来的,也不会受影响。
同样对于浮点型的变量类型,对应的 u1.v.type=IS_DOUBLE
,使用的是 value.dval
,而 dval
恰好是 double
型的。整体而言,PHP 7 对整型和浮点型变量的实现比较简单,非常容易理解。接下来讨论一下字符串类型变量的实现。
字符串类型
PHP 7 中定义了一个新的结构体 _zend_string
,第 4 章会详细介绍,这里简单说一下,其结构如下:
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
_zend_string
的头部维护着 gc
的信息,并且冗余了 hash
值 h
,这个操作据说为 PHP 7 提高了 5% 的性能,避免了在数组操作中 hash
值的重复计算。len
表示字符串的长度,val
记录了字符串的内容。这里的 val
值采用了柔性数组(被收入到 C99
标准中),这种方式相较于 PHP 5 中的字符串与结构体分离,读写的效率更高。
PHP 7 中的字符串是通过 zval.str
指向 zend_string
结构体的,如图3-9所示。

字符串类型有不同的属性维护在头部的 zend_refcounted_h
结构体中,比如 PHP 7 中有一种内部字符串 INTERNED STRING,而作用于字符串本身有 IS_STR_INTERNED
标志位,但不是 zval
的。字符串中还有一些类似的标志位:
IS_STR_PERSISTENT //是malloc分配内存的字符串
IS_STR_INTERNED //INTERNED STRING
IS_STR_PERMANENT //不可变的字符串,起哨兵作用
IS_STR_CONSTANT //代表常量的字符串
IS_STR_CONSTANT_UNQUALIFIED //带有可能命名空间的常量字符串
数组
数组是 PHP 代码中比较重要的一个结构,本质上 PHP 的数组是有序的字典,即它们表示 key-value 对的有序列表,其中 key-value 映射是使用 HashTable
实现的。PHP 将字符串 key
通过哈希函数运算返回一个整数。这个整数被用作 “普通” 数组的索引。但是带来新的问题是,两个不同的字符串可能得到相同的哈希值,因此这样的 HashTable
需要实现某种机制来解决冲突。
PHP 7 中 HashTable
的经过有了很大的改变,也为 PHP 7 带来了巨大的性能提升。本书第 5 章有详细阐述,这里暂不做讨论。这里想说的是 HashTable
结构体头部包含 gc
结构体,如下面的代码所示:
struct _zend_array {
zend_refcounted_h gc;
//…代码省略…//
头部的 gc
结构体解决数组的引用计数和循环引用的问题,3.4 节有详细讲解,暂不在这里展开。
引用
说到引用的问题,不得不说一下 PHP 中传值和传址的区别。
传值即 PHP 代码中的赋值操作,如上面的代码所示,当改变 $b
值时,$a
的值需要保持不变,因此需要在此分离。传址意味着当改变 $b
的值时,$a
也会跟着变。
从 PHP 7 的 zval
设计上可以看到,zval
没有存储引用计数的相关信息,所以在处理 “&”
符号引用的问题上,PHP 7 采用完全不同的一种方式。先来看看 PHP 5 是如何处理的。
PHP 5 在引入引用计数后,使用了 refcount__gc
来记录次数,同时使用 is_ref__gc
来记录是否是引用类型。以上面的例子为例:
$a = 'hello'; //$a -> zval1(type=IS_STRING, refcount__gc=1, is_ref__gc=0)
$b = $a; //$b, $a -> zval1(type=IS_STRING, refcount__gc=2, is_ref__gc=0)
$c = &$b; //$a -> zval1(type=IS_STRING, refcount__gc=1, is_ref__gc=0)
//$c, $b -> zval2(type=IS_STRING, refcount__gc=2, is_ref__gc=1)
将 $a
赋值给 $b
, refcount__gc
会增加,但是并不会改变引用类型。当使用 “&”
操作时,将 $b
赋值给 $c
, zval
的 is_ref__gc
值会改变,使得此时的 zval
必须进行分离,但是实际上它们的值还没有变化,这使得需要在堆中维护两个值为 “hello” 的 zval
。
PHP 7 引入了新的类型 IS_REFERENCE
来处理这个问题,首先看看 zend_reference
的结构体:
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
zend_reference
由记录 gc
信息的 zend_refcounted_h
结构体和 zval
结构体组成,由 val
来存储实际的值,zend_refcounted_h
结构体用来存储引用计数的信息。前面提到过,在 PHP 7 中复杂类型的引用计数的信息都记录在自身头部的 gc
中,zval
没有存储引用计数的字段,所以增加了这种结构用于垃圾回收。下面看看 PHP 7 中使用传址时变量结构的变化:
/* 上面提到的zend_string都是同一个地址 */
$a = 'hello'.time(); //$a -> zend_string(refcount=1, val)
$b = $a ; //$b, $a -> zend_string(refcount=2, val)
$c = &$b; //$a -> zend_string(refcount=2, val)
//$c, $b -> zval(type=IS_REFERENCE, refcount=2) -> zend_string(refcount=2, val)
从上面的流程中可以看出,当使用 “&”
操作时,会创建一种新的中间结构体 zend_reference
,这个结构体会指向真正的 zend_string
结构体,所以 zend_string
结构体的引用计数不变,同时 zend_reference
结构体的引用计数变为 2,因为 $c
和 $b
此时的类型都会变为 zend_reference
。这样的好处是原始的 zend_string
在内存中始终只有一份(避免了由于字符串的重复申请导致的内存浪费),更加易于维护。
在进行赋值时字符串只会增加引用计数,图3-10所示是正常的赋值。

当使用 “&”
后,会改变 “=”
号两边的变量的类型,新生成的类型指向字符串当前的地址,如图3-11所示。

间接 zval
从前面的学习中可以知道,PHP 7 中变量的类型有 20 种,而 PHP 5 中只有 11 种。常规类型是一些比较容易理解的类型,而内部类型绝对很陌生了。内部类型是对外无感知的,只在内部使用。PHP 7 中的内部类型都有哪些呢?请看下面。
#define IS_INDIRECT 15 // 间接zval
#define IS_PTR 17 // 指针zval
#define _IS_ERROR 20 // 内部使用的错误类型
上面的三种都是内部类型,但是本节主要说的是 IS_INDIRECT
类型。
PHP 代码通过词法和语法解析生成 AST
树(第 10 章会介绍如何通过词法和语法解析生成 AST
树),zend
引擎根据 AST
树生成 opcodes
数组(后面第 11 章也会介绍 PHP 7 是如何生成 op_array
的),zend
引擎模拟了一个执行栈来逐条执行 opcodes
数组中的 opcode
。在这里有一个需要注意的地方,在编译时已知的所有变量都被赋予一个索引,并且它们的值将存储在编译变量(CV
)表中。但是 PHP 也允许通过使用变量来动态地引用变量,或者如果在全局范围内,则使用 $GLOBALS
。如果发生这样的访问,PHP 将为函数/脚本创建一个符号表,其中包含从变量名到其值的映射。由于 CV
表不会在符号表的生命周期中重新分配,因此对于存储在 CV
表中的变量访问,符号散列表(symbol_table
)中的变量通过 INDIRECT
指向 CV
所在的位置。
为了便于理解,以一段 PHP 代码为例来讲解:
<?php
class User
{
function hello()
{
global $a;
$a = 'hello';
}
}
$obj = new User();
$obj->hello();
echo $a;
在上面的代码中,$a
只在 User
类的方法中出现,在调用 hello
方法之前,还不能直接访问。当使用关键字 global
后,$a
会在全局符号表和 CV
表中创建一个关系,这便是 INDIRECT
类型。为了更好地理解,通过 gdb
打印此时符号表中 “a”
的信息,具体如下所示:
(gdb) p *ex.symbol_table.arData[8].key
$9 = {gc = {refcount = 0, u = {v = {type = 6 '\006', flags = 2 '\002', gc_info = 0}, type_info = 518}}, h = 9223372036854953478, len = 1, val = "a"}
(gdb) p ex.symbol_table.arData[8].val
$10 = {value = {lval = 140737350021264, dval = 6.9533489732241371e-310, counted = 0x7ffff7c13090, str = 0x7ffff7c13090, arr = 0x7ffff7c13090, obj = 0x7ffff7c13090, res = 0x7ffff7c13090, ref = 0x7ffff7c13090, ast = 0x7ffff7c13090, zv = 0x7ffff7c13090, ptr = 0x7ffff7c13090, ce = 0x7ffff7c13090, func = 0x7ffff7c13090, ww = {w1 = 4156633232, w2 = 32767}}, u1 = {v = {type = 15 '\017', type_flags = 0 '\000', const_flags = 0 '\000', reserved = 0 '\000'}, type_info = 15}, u2 = {next = 4294967295, cache_slot = 4294967295, lineno = 4294967295, num_args = 4294967295, fe_pos = 4294967295, fe_iter_idx = 4294967295, access_flags = 4294967295, property_guard = 4294967295}}
这里需要注意的是 PHP 7 中的符号表是 HashTable(第 5 章会有详细的讲解),所以通过上面的形式可以取出。关于 CV
表,在第 11 章中会有阐述。
PHP 中的符号表 PHP 引擎在运行时维护了多个符号表,其中包括全局符号表和局部符号表。全局符号表存储了在全局作用域中声明的变量,而局部符号表则根据执行上下文动态创建和销毁,存储了函数、方法或代码块中的局部变量。 全局符号表的特点和作用
PHP 引擎内部实现 在 PHP 引擎的内部实现中,全局符号表是一个 |
在 PHP 中,CV 表(Compiled Variables Table)是一个与编译时变量相关的数据结构,用于在编译和执行脚本时高效管理变量。这种表格结构主要用于存储编译后的函数和方法中的局部变量和参数。 CV 表的作用 CV 表的主要作用是优化变量访问的速度和效率。与全局符号表不同,CV 表主要用于局部变量和参数的管理,特别是在函数和方法的上下文中。 工作原理 当 PHP 脚本被编译时,编译器会将每个函数和方法的局部变量和参数收集起来,并存储在一个数组中。这个数组就是 CV 表。每个变量在 CV 表中都有一个唯一的索引,通过这个索引可以快速访问和操作变量。 示例 假设有以下 PHP 代码:
在编译阶段, 使用 CV 表的优势
|
常量和常量AST
常量就是指值固定,在执行期间不会改变,这些固定的值也叫作字面量。PHP 底层在做词法和语法解析时会将字面量解析,并将其类型修改为 IS_CONST
。为了方便理解,以 PHP 代码为例:
<? php
$a = "hello";
上面示例中 “hello”
、“a”
就是常量,常量在 PHP 底层被标记为可引用和可复制的。常量 AST
树比较特殊,是用来标记特殊的 AST
树的,这种树可以有引用计数。它跟普通的 AST
树节点的区别是头部多了一个 zend_refcounted_h
结构,用于存储引用计数相关信息。它的结构如下:
struct _zend_ast_ref {
zend_refcounted_h gc;
zend_ast *ast;
};
typedef struct _zend_ast {
zend_ast_kind kind; // AST 节点类型
zend_ast_attr attr; // 节点属性
uint32_t lineno; // 行号
uint32_t children; // 子节点数量
zend_ast *child[1]; // 子节点数组
} zend_ast;
普通的 zend_ast
节点通过底层定义的宏可以转换成常量 AST
类型。该宏的定义如下所示:
#define ZVAL_NEW_AST(z, a) do { \
zval *__z = (z); \
zend_ast_ref *_ast = \
(zend_ast_ref *) emalloc(sizeof(zend_ast_ref)); \
GC_REFCOUNT(_ast) = 1; \
GC_TYPE_INFO(_ast) = IS_CONSTANT_AST; \
_ast->ast = (a); \
Z_AST_P(__z) = _ast; \
Z_TYPE_INFO_P(__z) = IS_CONSTANT_AST_EX; \
} while (0)
|
常量 AST
也是可引用和可拷贝的,这个跟常量是一样的。
在 PHP 中,常量 示例比较 普通 AST 树示例
生成的普通 AST 树可能是:
常量 AST 树示例
生成的常量 AST 树可能是:
在这个常量 普通 |
资源类型
文件句柄、Socket
链接等是资源类型,通过 void *
指针可以指向任何结构体。资源类型的数据结构如下:
struct _zend_resource {
zend_refcounted_h gc; /* 引用计数和垃圾回收信息 */
int handle; /* 资源句柄 */
int type; /* 资源类型 */
void *ptr; /* 指向实际资源的指针 */
};
资源类型使用的地方比较广,在使用时根据不同的类型对 void *
指针进行强制转换。
对象
在 PHP 5 时代,对象存储在 _zend_object_value
结构中:
typedef struct _zend_object_value {
zend_object_handle handle;
const zend_object_handlers *handlers;
} zend_object_value;
handle
是一个无符号 int
,通过 handle
可以在全局的对象池里索引到指定对象。handlers
指向一个包含多个函数指针的结构体,比如对象的析构、释放、读属性等操作函数。但是对象的真正数据并没有在这里,而是存在全局的 EG(objects_store)
中,objects_store
数据结构如下:
typedef struct _zend_object_store_bucket {
zend_bool destructor_called;
zend_bool valid;
zend_uchar apply_count;
union _store_bucket {
struct _store_object {
void *object;
zend_objects_store_dtor_t dtor;
zend_objects_free_object_storage_t free_storage;
zend_objects_store_clone_t clone;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *buffered;
} obj;
struct {
int next;
} free_list;
} bucket;
} zend_object_store_bucket;
typedef struct _zend_objects_store {
zend_object_store_bucket *object_buckets; /* 对象存储桶数组 */
zend_uint top; /* 下一个可用槽的位置 */
zend_uint size; /* 存储桶数组的大小 */
int free_list_head; /* 自由列表头,用于重用已释放的槽 */
} zend_objects_store;
要获取 object
,需要通过下面这种方式,经过多次查找:
EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj
不同于 zval
的一点是,对象在 zend_object_store_bucket
桶中维护了另外一个 refcount
来记录对象的引用计数,以保证其在垃圾回收时可以正常被回收。两套引用计数和获取对象的多次内存读取,使得对象效率比较低。
PHP 7 中的对象试图解决上面的问题,从它的结构可以看出与 PHP 5 中对象的区别,PHP 7 中对象的结构如下所示:
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; //对象句柄 TODO: may be removed ? ? ?
zend_class_entry *ce; /* 指向对象所属类的 zend_class_entry 结构体的指针。通过该指针,可以访问对象的类名、属性和方法 */
const zend_object_handlers *handlers; /* 指向对象处理程序的指针。对象处理程序定义了对象的行为,例如访问属性、调用方法等 */
HashTable *properties;
zval properties_table[1]; /* 属性数组,这个数组用于动态管理对象的属性,使得 PHP 可以支持灵活的属性添加和删除*/
};
PHP 7 中对象的属性数据存储在 properties_table
数组中,而 properties
是一个 HashTable
,它的 key
为对象的属性名,value
为属性值在 properties_table
数组中的偏移量,通过偏移量可以在 properties_table
数组中取到真正的数据。为什么这样设计呢?在第 6 章有详细的讲解,这里就不展开了。
另外对象结构体的头部也包含了引用计数的信息,如前面所说,复杂类型的引用计数都是由自身来维护的。头部的 gc
结构体解决了 PHP 5 中重复计数的问题。
与 PHP 5 中的对象结构体进行比较,有没有觉得 PHP 7 中的对象更加简洁呢?结合 zval
支持对象类型 IS_OBJECT
,获取一个对象时通过 value.obj
读取内存一次即可,有没有感觉清爽了很多呢?
知识
zend_always_inline 和 zend_always_inline
define zend_always_inline inline __attribute__((always_inline))
define zend_never_inline __attribute__((noinline))
-
zend_always_inline
宏通过结合inline
关键字和__attribute__((always_inline))
属性,强制编译器内联指定的函数,以优化性能,特别是在 PHP 的 Zend 引擎中,频繁调用的小函数可以通过这种方式减少函数调用的开销。 -
zend_never_inline
宏通过使用__attribute__((noinline))
属性,明确告诉编译器不要内联指定的函数。这在调试、特定性能优化和函数调用管理等场景下非常有用,确保函数始终作为独立的调用存在。
常用的宏
ZEND_SAME_FAKE_TYPE
#define ZEND_SAME_FAKE_TYPE(faketype, realtype) ( \
(faketype) == (realtype) \
|| ((faketype) == _IS_BOOL && ((realtype) == IS_TRUE || (realtype) == IS_FALSE)) \
)
ZEND_SAME_FAKE_TYPE
宏用于比较 PHP 数据类型,特别是处理布尔类型的情况。它在需要判断两个类型是否相同时非常有用,包括基本类型比较和布尔类型的特殊处理。这个宏通过简化类型比较逻辑,提高了代码的可读性和维护性。
下面是一个使用 ZEND_SAME_FAKE_TYPE
宏的完整示例:
#include <stdio.h>
#define IS_LONG 1
#define IS_STRING 2
#define _IS_BOOL 3
#define IS_TRUE 4
#define IS_FALSE 5
#define ZEND_SAME_FAKE_TYPE(faketype, realtype) ( \
(faketype) == (realtype) \
|| ((faketype) == _IS_BOOL && ((realtype) == IS_TRUE || (realtype) == IS_FALSE)) \
)
void check_type(int faketype, int realtype) {
if (ZEND_SAME_FAKE_TYPE(faketype, realtype)) {
printf("Types match: faketype = %d, realtype = %d\n", faketype, realtype);
} else {
printf("Types do not match: faketype = %d, realtype = %d\n", faketype, realtype);
}
}
int main() {
int faketype1 = IS_LONG;
int realtype1 = IS_LONG;
int faketype2 = _IS_BOOL;
int realtype2 = IS_TRUE;
int faketype3 = _IS_BOOL;
int realtype3 = IS_FALSE;
int faketype4 = IS_STRING;
int realtype4 = IS_TRUE;
check_type(faketype1, realtype1); // Types match: faketype = 1, realtype = 1
check_type(faketype2, realtype2); // Types match: faketype = 3, realtype = 4
check_type(faketype3, realtype3); // Types match: faketype = 3, realtype = 5
check_type(faketype4, realtype4); // Types do not match: faketype = 2, realtype = 4
return 0;
}