1. 什么是引用计数
在《php7 zval及变量存储方式》的2.3节中我们说到,对于复杂类型的变量(string,array,object,resource等),我们会将其具体的值记录在单独的内存区域,再由zend_value中相应的指针指向该内存区域。指向该内存区域的指针数量,即为引用计数。
引用计数是服务于垃圾回收的机制的。当引用计数为0,相应的内存区域就可以回收了。
官方手册中有关于引用计数的阐述,不过应该是针对5.*版本的,和7.*相比,大体思想是一样的,但实现和使用xdebug_debug_zval()得到的结果差别非常大。
2. 如何查看引用计数
安装xdebug后,可以使用xdebug_debug_zval()查看变量的引用计数。比如:
$a = 123;
xdebug_debug_zval('a');
输出
a: (refcount=0, is_ref=0)=123
refcount表示引用计数, is_ref表示是否为引用类型。
3. 简单类型的引用计数
我们说的简单类型是指:bool(true/false), null, long,double
对于这些类型的变量值,直接使用zval结构就可以记录,无需额外的内存。所以,也就没有引用计数。
更深层的原因是,php7开始,zval是在栈空间分配的,可自动释放,不需要垃圾回收(堆上的内存才需要主动管理回收),也就不需要引用计数了。
3.1 赋值
$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即可。
3.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对引用的实现有关。
3.2.1 引用类型
为了实现引用 ,php7引入了内部类型–引用类型,其结构体如下:
typedef struct _zend_reference zend_reference;
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
- gc: 与垃圾回收相关
- val: 一个zval结构
3.2.2 对例子的解析
对于3.1中普通赋值的例子,赋值后,$a, $b是两个独立的zval结构,情形如下:
对于3.2中引用赋值的例子,php创建了一个引用类型的结构体,$a, $b中的value.ref均指向它,情形如下:
所以我们看到a,b的引用计数为2,且都为引用类型。
思考:如果接下来我们执行如下代码,得到什么结果?
unset($a);
xdebug_debug_zval('b');
4. 字符串的引用计数
仍然先看例子:
$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呢?
4.1 字符串的类型
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时计算出来并动态分配内存存储,需要引用计数。
4.2 对例子的解释
$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。
5. 数组的引用计数
5.1 不可变数组
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')
说明:
- $a为不可变数组,所以引用计数为2
- b = b= b=a赋值后,两者的zval.value.arr指向同一个zend_array, 引用计数+1, 所以a,b的引用计数都为3。
5.2 普通数组
如何产生一个普通数组(非不可变数组)呢?
- 动态生成数组
- 对不可变数组做任何改变(增减元素,改变元素值)
看下面的例子
$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)
- $c是一个普通数组,引用计数为1。
- j = j= j=c后,引用计数+1,所以两者引用计数均变为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')
说明:
- 起初a为不可变数组,引用计数为2
- b = b= b=a后,引用计数+1,故为3
- 改变 a [ 0 ] 后 ( ∗ ∗ 注 意 , 此 处 发 生 了 c o w 写 时 拷 贝 , 即 a[0]后(**注意,此处发生了cow~写时拷贝,即 a[0]后(∗∗注意,此处发生了cow 写时拷贝,即a完全复制之前的数组,再修改第0个元素**), a 成 为 普 通 数 组 , 引 用 计 数 为 1 。 a成为普通数组,引用计数为1。 a成为普通数组,引用计数为1。b指向原有的不可变数组,引用计数为2。
6. 类的引用计数
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,比较好理解。
7. 总结
- 变量是简单类型(true/false/double/long/null)时直接拷贝值,不需要引用计数。
- 变量是临时字串,在赋值时会用到引用计数,但如果变量是字符常量(字串字面量),则不会用到。
- 变量是对象(zval.u1.v.type=IS_OBJECT), 资源(zval.u1.v.type=IS_RESOURCE), 引用(zval.u1.v.type=IS_REFERENCE)类型时, 赋值一定会用到引用计数。
- 变量是普通的数组, 赋值时会用到引用计数,变量是IS_ARRAY_IMMUTABLE时,赋值不使用引用计数。
| 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)
8. 引用计数记录在哪里
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,就是用来记录引用计数的具体数值的。