本次学习使用的php版本是php7.2.6
当我们谈及一门编程语言的变量,都会想到它的三个基本组成部分:变量名,变量类型,变量值,一直很好奇PHP的底层是怎么去表示变量的,所以有时间去学习了一下它的源码。
在PHP的底层实现变量存储是使用一种数据结构zval,这个结构同时还保存着PHP中的各种数据类型,我们可以在Zend目录下找到zend_types.h文件,此文件便定义了该数据结构
对应的代码如下:
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4( //这个是为了兼容大小字节序,小字节序就是下面的顺序,大字节序则下面4个顺序翻转
zend_uchar type, //变量类型
zend_uchar type_flags, //类型掩码,不同的类型会有不同的几种属性,内存管理用到
zend_uchar const_flags, //
zend_uchar reserved) /* call info for EX(This),zend执行流程用到 */
} v;
uint32_t type_info; //上面4个值的组合值,可以直接根据type_info取到4个对应位置的值
} u1;
union {
uint32_t next; /* hash collision chain */
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 */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t extra; /* not further specified */
} u2;
};
其中zend_value是一种联合体,是一个size_t大小(一个指针大小), 可以保存一个指针, 它仅仅会直接存储一个long, 或者一个double,对于其他类型则直接存储对应的指针,对应代码如下:
typedef union _zend_value {
zend_long lval; //整型
double dval; //浮点型
zend_refcounted *counted; //
zend_string *str; //string字符串
zend_array *arr; //array数组
zend_object *obj;
zend_resource *res;
zend_reference *ref; //引用类型
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;
zval结构如上,内嵌一个union联合体的zend_value保存具体变量类型的值或者指针,另外两个联合体
u1:u1.v.type用来区分变量类型,type_flags为类型掩码,用在变量内存管理和gc机制中。
u2是各种辅助字段,因为value成员占一个指针大小8字节,而使用的是结构体是内存对齐的,所以假如zval结构体只有value和u1两个值,整个zval依旧会对齐到16byte,所以把多余的4byte拿出来用于特殊用途是划算的
而PHP的zval类型有如下:
/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#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
/* constant expressions */
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 19
#define IS_VOID 18
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
#define _IS_ERROR 20
从PHP7开始,对于zval的value字段能够保存下来的值不会再进行引用计数,而是在拷贝的时候直接赋值,这样的类型有IS_LONG,IS_DOUBLE,IS_NULL,IS_FALSE,IS_TRUE
而对于复杂的类型,一个size_t保存不下的就用value来保存这个指针,指向具体的值,引用计数的值也作用在这个值上而不是zval上。
以数组为例:
zval.value.arr将会指向下面这样一个结构体,由它来保存一个数组,引用计数部分保存在zend_refcounted_h结构中。
struct _zend_array {
zend_refcounted_h gc; //引用计数信息
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consistency)
} v;
uint32_t flags;
} u;
uint32_t nTableMask; //计算bucket索引时的掩码
Bucket *arData; //bucket数组
uint32_t nNumUsed; //已用bucket数组
uint32_t nNumOfElements; //已有元素数,nNumOfElements <=nNumUsed,因为删除的并不是直接从arData中移除
uint32_t nTableSize; //数组的大小,为2^n
uint32_t nInternalPointer; //数值索引
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
如上,每一个指针类型指向的结构体都会引用计数并且都有一个相同结构体如下:
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
引用计数
下面我们看一个例子来搞懂引用计数问题,在这之前必须有xdebug的扩展
<?php
$a = '现在时间戳是'.time();
xdebug_debug_zval( 'a' );
$b = $a;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
$c = $b;
xdebug_debug_zval( 'a' );
xdebug_debug_zval('b');
xdebug_debug_zval( 'c' );
如上使用xdebug_debug_zval函数调试可以看到如下输出内容:
引用计数是指在value中增加一个字段你refcount记录来指向当前value的数量,变量复制、函数传参时并不需要直接硬拷贝一份value数据,而是直接refcount++,变量销毁时相应减去,而等到refcount的值为0时候表示没有变量引用这个value,将它销毁即可。
我们从先前也可以知道对于long、double是直接硬拷贝的,也就是说并不是所有的数据类型都会用到引用计数,只有value是指针的那几种类型才有可能会用到引用计数。
怎么理解?可以先看下面这个例子:
<?php
$a = "lanco";
xdebug_debug_zval( 'a' );
$b = $a;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
$c = $b;
xdebug_debug_zval( 'a' );
xdebug_debug_zval('b');
xdebug_debug_zval( 'c' );
你认为输出会是什么?很简单呀,后三个输出都是refcount=3的,emmm,这样就算了
很惊喜很意外,引用计数为0,为什么?--说实话当初也让我眉头紧皱了一会
事实上并不是所有的PHP变量都会用到引用计数,标量true/false/double/long/null是硬拷贝不需要这种机制,但是除了这几种还有两个特殊的类型也不会用到:
1、interned string(内部字符串,在zend_types.h中还有定义字符串flag)
2、immutable array 在opcache会用到这种类型
但是他们的type是IS_STRING和IS_ARRAY,这与普通的字符串数组相同,怎么去区分一个value是否支持引用计数呢?这里就要用到zval.u1这个联合体了的type_flag这个类型掩码了。正是通过这个字段,而这个字段除了标识value是否支持引用计数之外还有其他的标志位:
/* zval.u1.v.type_flags */
#define IS_TYPE_CONSTANT (1<<0)
#define IS_TYPE_REFCOUNTED (1<<2)
#define IS_TYPE_COPYABLE (1<<4)
下面是具体哪一些类型会有这个标识
| type | refcounted | +----------------+------------+ |simple types | | |string | Y | |interned string | | |array | Y | |immutable array | | |object | Y | |resource | Y | |reference | Y |
interned string:内部字符串,什么意思,我们可以这么认为在PHP写的所有字符都是这种类型,像上面例子:$a="lanco"后面的字符串内容是唯一不变的,这些字符串等同于c语言中定义在静态变量区的字符串,生命周期在整个请求,在请求结束之后会统一销毁,自然也就是无需在运行期间通过引用计数来管理内存
immutable array:暂时没有查阅到资料关于这种类型,日后补上
引用:
我们都知道在PHP中通过&操作符产生引用变量,它首先会创建一个zend_reference结构,内嵌一个zval,这个zval的value指向原先zval的value,如果是前面所讲的简单类型则直接复制值,然后将原先的zval类型修改为IS_REDERENCE,原先的zval的value会指向新建的zend_reference结构。
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
不妨猜测下面的输出内容分别是什么吧:
<?php
$a = "time:" . time();
$b = &$a;
$c = $b;
xdebug_debug_zval( 'a' );
xdebug_debug_zval('b');
xdebug_debug_zval( 'c' );
echo "<hr />";
$d = "time:" . time();
$e = &$d;
$f = &$e;/*或$c = &$a*/
xdebug_debug_zval( 'd' );
xdebug_debug_zval('e');
xdebug_debug_zval( 'f' );
-------------------------------分割线----------------------------------------------------------------------------------------
答案如下:
也就是说在$b = &$a的时候,$a,$b类型是引用,但是$c = $b并不会直接将$b赋给$c,而是把$b的实际指向zval赋给$c,而如果你希望$c也是一个引用就需要$c = &$b。以上也表示PHP中引用只会有一层,不会出现c中指针的指针这种引用传递。
写时复制:
前面提到多个变量指向同一个value,通过recount统计引用计数,在这时如果一个变量修改了value,那么他就会拷贝一份并断开旧的连接,举例如下:
$a = array(1,2,3,4);
$b = &$a;
$c = $c;
$b[]=5;
$b的改变并不会改变$c.
如若您对上文任何地方有异议,请您指出,一定虚心聆听^^