PHP源码分析(基本变量、垃圾回收)

小而巧的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中 除了 arrayobject类型的变量,其余大部分是自动回收,在自动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缓冲区全部为真真的垃圾,然后将这些垃圾释放,回收完成

发布了83 篇原创文章 · 获赞 87 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/ligupeng7929/article/details/90041631