小而巧的zval
扩充:
结构体: 比如
struct test { char a //1 int b//4 long c //8 }
总共占了2*8=16字节
因为结构体对齐,虽然浪费字节,但是得益于内存对齐,存取速度会更快
联合体:比如 union{
char a; //1 int b;4 long c ; //8
}
结果:(gdb) p sizeof(union test) $1=8
它复用了同一块内存,a、b和c公用同一块内存,修改a,也会修改b和c的值,同时可以知道联合体的大小为其最大成员的大小
栈区:存储参数值、局部变量,维护函数调用关系,栈的变量是局部的,随着局部空间的销毁而销毁,由系统负责
堆区:动态内存区域,随时申请和释放,程序自身要对内存泄露负责,堆上面的变量可以提供全局访问,需要自行处理其生命周期
zval可以表示PHP中任意一个变量
struct_zval_struct{
zend_value value;
union u1;
union u2;
}
typedef union_zend_value{
zend_long lval; //整形
double dval; //浮点型
zend_refcounted *counted
zend_string *str; //字符串
zend_array *arr; //数组 ->hashtable
zend_object *obj; //对象
zend_resource *res; //资源类型
zend_reference *ref; //引用类型
zend_ast_ref *ast; //抽象语法树
zval *zv; //还可以指向一个zval
void *ptr; //不确定类型
zend_class_entry *ce;//类
zend_function *func;
}zend_value;
源码图:文件zand_types.h
u1结构体:
type 定义了变量的类型
type_flags 是变量类型特有的标记,可以表示常量、不可变的类型(存在共享内存中的数组)、需要引入计数的类型、可能包括循环引用的类型(is_array,is_object)、可被复制的类型
const_flags:是常量类型特有的标记,0变量 2常量
reserved: 是保留字段
在PHP7中,通过value和u1已经可以表示任何类型,并记录一些类型的属性。另外还有一个u2其实是增加辅助字段。
u2结构体:
next:是主要解决hash冲突的,记录冲突的下一个元素位置
cache_slot:主要是做运行时缓存的,在执行函数的时候优先区缓存查找,若缓存没有,会在全局function 表中查找
lineno 标记了哪一行,一般用作AST抽象语法树
num_args:代表函数调用输入参数的个数
fe_pos:代表我们foreach的时候位置,每foreach一次这个值+1,当再次调用foreach对数组进行遍历,会首先对这个指针重置。
fe_iter_idx:也在foreach中使用,代表游标索引的位置
access_flags:主要用在类里面比如写代码用到的public、protected等
property_guard:防止类中魔术方法的循环引用,在get和set中会用到
其中zval里面有三个变量,他们都是联合体, value里面有各种类型的指针,它的类型判断是由u1里面的type来判断的,根据type来取里面不同的值,比如type里面是IS_LONG整形,可以直接取zend_value里面的lval,就得到值了,如果type是string,我们取*str,它是一个指向zend_string的指针
变量都挨在一起放在同一段内存中,每一个都占了16字节
在PHP5中,所有的变量都是在堆中申请,但是对于临时变量是没有必要的,而PHP7进行了优化,这种临时变量直接在栈中申请。
1、从源码中我们看到虽然说PHP是弱语言类型,但是在真正的底层实现还是区分类型的,为什么需要区分类型呢?
如果知道一个变量想知道它的长度的话有这么几种方法,一个是专门用一个长度的字段来记录长度比如我们字符串,数组的话我们有一个长度的字段,其他的我们用类型这个字段,而类型天然有长度这个字段,一个类型隐世的包含它的长度,比如我们定义 $a=1虽然我们没有定义类型,但是底层解析会把他定义为整形,会用到zend_value 的lval
2、 如何区分类型:看u1里面的结构体v的type,这个type就是代表不同的类型,类型定义了有IS_FALSE、IS_TRUE、IS_LONG、IS_ARRAY、IS_OBJECT等,另外还有一个type_info,可以快速的取出上面的四个char的值
Zend_string 字符串
注:_zend_string头部维护了gc的信息,并且冗余了hash值h,这个操作据说为PHP7提高了5%的性能,避免在数组操作中hash值的重复计算
gc对应一个结构体,主要是进行垃圾回收的,refcount为引用计数
h 为这个字符串对应的hash值,后面会用在数组里面
len和val[1]:是二进制安全的,不像c语言中如果/0就会被截断,两个就可以表示一个字符串,其中gc和h主要用作垃圾回收和以空间换时间做hash运算时存储的一个h值
写时复制:如果是整形或者其他简单的类型,用zval的16个字节就可以表示,所以是直接赋值的,比如zend_string,$a='string',$b=$a,他们的*str指向同一个zend_string,使用gc里面的refcount+1,当修改b的时候进行copy一份出来进行修改
对于7.1.0的话 refcount=0,flags=2常量 变量的话 refcount=1,flags=0,并且字符串是写时复制的,当没有修改前的话,他们的*str都指向同一个zend_string,修改后refcount-1,并且分配一个新的地址
引用类型:
在zend_value里面
zend_reference *ref 对应 IS_REFERENCE
当把$b=&$a的的时候,a的type也变成10,引用类型了,都是同一个地址
$a='hello' //$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记录着gc信息的zend_refcounted_h结构体和zval结构体组成。由zval储存实际的值,zend_refcounted_h结构体用来存储引用计数的信息。在PHP 7中复杂类型的引用计数信息都记录在自身头部的gc里面,zval没有存储引用计数的字段,所以增加了这种结构用来垃圾回收
复制和引用:
当$a复制string的时候,$a对应的zval的类型为IS_STRING,指向的是zend_string,当$b=&$a的时候,它们的类型都变成了zend_reference,它们指向同一个zend_reference,因为zend_reference里面有zval和gc,它的类型为IS_STRING,指向真正的zend_string,所以zend_string结构体的引用计数不变,同时zend_reference的结构体引用计数变成2。这样做的好处是原始的zend_string在内存中始终只有一份(避免了由于字符串的重复申请导致的内存浪费)。 当使用unset($b)时候,只是把b的zval的type类型改为NULL,甚至ref地址的指针都没有变,而a的zval的type还是为10,类型为ref没有变,指向的zend_string也没有变
Zend_array 数组
zend_array 还有个别名 HashTable
TableMask:计算索引值
*arDate:真正存储的是key-value对
NumUsed:已经用过的空间
TableSize:代表的是arDate的大小,初始化的话大小为8,当不够用的话进行扩容8-16-32
NextFreeElement:当我们直接赋值没有key的value
整个HashTable分为了Bucket arr和hash arr
Bucket array是后面存arrData的下标一个个往上加的,0123,我写进去的时候他们key一定是数字,这个arData前面有一个索引的数组,只用两个位置,分别是-1和-2,这个是Bucke array
Hash array是算出来的hash值& nTableMask得到一个值
垃圾回收
在PHP 7中复杂类型的引用计数信息都记录在自身头部的gc里面,zval没有存储引用计数的字段,所以增加了这种结构用来垃圾回收,PHP7垃圾回收的实现方法是定期遍历和标记若干存储对象的数组,在通过算法将是垃圾的物理空间回收。
在php中 除了 array
和object
类型的变量,其余大部分是自动回收,在自动GC机制中,在zval断开value的指向时如果发现refcount=0则直接释放value,这时变量的回收时机,发生断开的这两种常见的情况是修改变量与函数返回时,修改变量会断开原有value的指向,函数返回时会释放所有的局部变量,也就是把所有局部变量的引用计数减一
$a = 1;
$b = $a;
xdebug_debug_zval('a');
$a =10;
xdebug_debug_zval('a');
unset($a);
xdebug_debug_zval('a');
结果
a:
(refcount=2, is_ref=0),int 1
a:
(refcount=1, is_ref=0),int 10
a: no such symbol
可以看到 当$a =10 的时候 涉及到 php的COW(copy-on-write)机制,$b 会复制一份原先的 $a ,解除了他们之间的引用关系,所以a的引用次数(refcount)减少为1。
然后我们uset($a)之后 a的引用次数变为0。这就会被认为是垃圾变量,释放空间。
还有一种情况是这个机制无法解决的,从而因变量无法回收导致内存始终得不到释放,造成内存泄露,这种情况就是循环引用。也就是变量的内部引用了变量本身。
例如:
$a = [1];
$a[1] = &$a;
unset($a);
在 unset($a) 之前 $a 的类型为引用类型
a:
(refcount=2, is_ref=1),
array (size=2)
0 => (refcount=1, is_ref=0),int 1
1 => (refcount=2, is_ref=1),
&array<
unset($a) 之后,就变成这样
这时候,我们unset操作时refcount 由2变为1,所有的外部引用都断开的时候,因为有内部引用指向 $a,数组的refcount仍然大于0而得不到释放
它就成了一个“孤儿”,在c语言中叫做野指针。在php中叫做循环引用。内存泄漏。想要销毁变量的话,只能等 php脚本结束。
循环引用造成的内存泄漏
为了清理这些垃圾,引入了两个准则
如果引用计数减少到零,所在变量容器将被清除(free),不属于垃圾
如果一个zval 的引用计数(refcount)减少后还大于0,那么它可能会是一个垃圾。
循环引用基本上只会出现在 数组和对象中,对象是因为它的本身就是引用,针对第一种情况的话,垃圾回收期不会处理,只有第二种情况的话会将垃圾收集起来。
Object的情况是则是成员属性引用对象本身导致的,其他类型不会出现这种变量中的成员引用自身的情况,所以垃圾回收只处理这两种类型
object和array的回收过程
php7的垃圾回收包含两个部分,一个是垃圾收回收期,一个是垃圾回收算法。
垃圾收集器,把可能是垃圾的元素收集到回收池中 也就是把变量的 zend_refcount>0
的变量 放在一个buffer缓存区中。
垃圾回收的算法:等到回收池的值达到一定额度了,会进行统一遍历把当前value标为灰色(zend_refcounted_h.gc_info置为GC_GREY),然后对当前value的成员进行深度深度优先遍历,把所有成员变量value的refcount-1,进行模拟删除,如果当前value的zend_refcount=0
那就认为是垃圾,直接删除它(标记为白色),如果不为0,则排除了引用全部来自于自身成员的可能,表示还有外部的引用,并不是垃圾,然后需要还原,对所有成员进行深度遍历,把成员refcount+1,同时标记为黑色。
然后再次遍历buffer,将非GC_WHITE的节点从Buffer中删除,最终buffer缓冲区全部为真真的垃圾,然后将这些垃圾释放,回收完成