在《php7 zval及变量存储方式》的2.3节中我们说到,对于复杂类型的变量(string,array,object,resource等),我们会将其具体的值记录在单独的内存区域,再由zend_value中相应的指针指向该内存区域。指向该内存区域的指针数量,即为引用计数。
引用计数是服务于垃圾回收的机制的。当引用计数为0,相应的内存区域就可以回收了。
官方手册中有关于引用计数的阐述,不过应该是针对5.*版本的,和7.*相比,大体思想是一样的,但实现和使用xdebug_debug_zval()得到的结果差别非常大。
安装xdebug后,可以使用xdebug_debug_zval()查看变量的引用计数。比如:
$a = 123;
xdebug_debug_zval('a');
输出
a: (refcount=0, is_ref=0)=123
refcount表示引用计数, is_ref表示是否为引用类型。
我们说的简单类型是指:bool(true/false), null, long,double
对于这些类型的变量值,直接使用zval结构就可以记录,无需额外的内存。所以,也就没有引用计数。
更深层的原因是,php7开始,zval是在栈空间分配的,可自动释放,不需要垃圾回收(堆上的内存才需要主动管理回收),也就不需要引用计数了。
$a = 6.2;
$b = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出
a: (refcount=0, is_ref=0)=6.2
b: (refcount=0, is_ref=0)=6.2
b = b= b=a变量赋值时,新申请一个zval,使zval.value.dval=6.2即可。
先来看例子:
$a = 6.2;
$b = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出:
a: (refcount=2, is_ref=1)=6.2
b: (refcount=2, is_ref=1)=6.2
a,b都变为引用类型,引用计数都变成了2,这是为什么呢。这与php7对引用的实现有关。
为了实现引用 ,php7引入了内部类型–引用类型,其结构体如下:
typedef struct _zend_reference zend_reference;
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
对于3.1中普通赋值的例子,赋值后,a, b是两个独立的zval结构,情形如下:
对于3.2中引用赋值的例子,php创建了一个引用类型的结构体,a, b中的value.ref均指向它,情形如下:
所以我们看到a,b的引用计数为2,且都为引用类型。
思考:如果接下来我们执行如下代码,得到什么结果?
unset($a);
xdebug_debug_zval('b');
仍然先看例子:
$h = "time";
$i = $h;
xdebug_debug_zval('h');
xdebug_debug_zval('i');
$y = "time".time();
$x = $y;
xdebug_debug_zval('x');
xdebug_debug_zval('y');
输出
h: (interned, is_ref=0)='time'
i: (interned, is_ref=0)='time'
x: (refcount=2, is_ref=0)='time1598507697'
y: (refcount=2, is_ref=0)='time1598507697'
为什么同样是字符串,$h, i 没 有 引 用 计 数 。 而 i没有引用计数。而 i没有引用计数。而y, $x的引用计数为2呢?
zend_types.h中做了如下定义,注意,这个类型并不是记录在zval.u1.v.type中的,而是记录在zval.value->gc.u.flags中,主要服务于垃圾回收的。
/* string flags (zval.value->gc.u.flags) */
#define IS_STR_INTERNED GC_IMMUTABLE /* interned string */
#define IS_STR_PERSISTENT GC_PERSISTENT /* allocated using malloc */
#define IS_STR_PERMANENT (1<<8) /* relives request boundary */
具体的字串类别见下图(参考《php7底层设计与源码实现》4.3.2 字符串的类别)
其中,内部字串和已知字串,都会存在于php运行的整个周期,不涉及垃圾回收问题,自然也不需要引用计数。
临时字串,只能在虚拟机执行opcode时计算出来并动态分配内存存储,需要引用计数。
$h = "time";
$i = $h;
time就属于字串字面量,所以得到
h: (interned, is_ref=0)='time'
i: (interned, is_ref=0)='time'
$y = "time".time();
$x = $y;
time()只能在运行时计算,所以"time".time()是临时字串。赋值后存储情况如下图所示:
因而引用计数为2。
php7中引入了不可变数组(immutable array)的概念。一个不可变数组,是由不可变元素构成的,这些元素在编译阶段就可完全解析确定,比如string, integer, float等等。引入这种类型主要是为了优化内存。对于不可变数组,规定其初始引用计数为2.
下面看一个例子:
$a = [1,2.1,'x'];
xdebug_debug_zval('a');
$b = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出结果:
a: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2.1, 2 => (interned, is_ref=0)='x')
a: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2.1, 2 => (interned, is_ref=0)='x')
b: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2.1, 2 => (interned, is_ref=0)='x')
说明:
如何产生一个普通数组(非不可变数组)呢?
看下面的例子
$c = range(1,2);
xdebug_debug_zval('c');
$j = $c;
xdebug_debug_zval('j');
xdebug_debug_zval('c');
输出:
c: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2)
j: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2)
c: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2)
接着我们看一个复杂些的例子。
$a = ['y', 'x'];
xdebug_debug_zval('a');
print("after b=a\n");
$b = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
print("after change a[0]\n");
$a[0] = 'b';
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出
a: (refcount=2, is_ref=0)=array (0 => (interned, is_ref=0)='y', 1 => (interned, is_ref=0)='x')
after b=a
a: (refcount=3, is_ref=0)=array (0 => (interned, is_ref=0)='y', 1 => (interned, is_ref=0)='x')
b: (refcount=3, is_ref=0)=array (0 => (interned, is_ref=0)='y', 1 => (interned, is_ref=0)='x')
after change a[0]
a: (refcount=1, is_ref=0)=array (0 => (interned, is_ref=0)='b', 1 => (interned, is_ref=0)='x')
b: (refcount=2, is_ref=0)=array (0 => (interned, is_ref=0)='y', 1 => (interned, is_ref=0)='x')
说明:
class Demo{
public $name = "ball";
}
$a = new Demo();
$b = $a;
结果
a: (refcount=2, is_ref=0)=class Demo { public $name = (refcount=0, is_ref=0)='ball' }
b: (refcount=2, is_ref=0)=class Demo { public $name = (refcount=0, is_ref=0)='ball' }
a, b的zval.value.obj指向同一个zend_object, 因此引用计数为2,比较好理解。
| type | refcounted |
+----------------+------------+
|simple types | |
|string | Y |
|interned string | |
|array | Y |
|immutable array | |
|object | Y |
|resource | Y |
|reference | Y |
当前变量若支持引用计数, 则zval.u1.v.type_flag包含 (注意是&,不是等于)IS_TYPE_REFCOUNTED
#define IS_TYPE_REFCOUNTED (1<<2)
php7将引用计数记录在具体的类型结构体中。
来看下Zend/zend_types.h中对string,array,object,resource,reference种支持引用计数的类型的定义:
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
struct _zend_array {
zend_refcounted_h gc;
... ...
};
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
struct _zend_resource {
zend_refcounted_h gc;
int handle; // TODO: may be removed ???
int type;
void *ptr;
};
struct _zend_reference {
zend_refcounted_h gc;
zval val;
zend_property_info_source_list sources;
};
每种类型中都包含
zend_refcounted_h gc
这是一个专门服务于垃圾回收的结构,定义如下:
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
uint32_t type_info;
} u;
} zend_refcounted_h;
其中的refcount,就是用来记录引用计数的具体数值的。