关于php:PHP弱类型变量实现原理zval垃圾回收refcountgc写时拷贝copyonwrite机制isrefgc

28次阅读

共计 4838 个字符,预计需要花费 13 分钟才能阅读完成。

对于 PHP 的弱类型变量实现原理

对于 PHP 的弱类型变量实现原理 其实曾经有很多文章了,这里只是做个总结,不便当前疾速回顾;毕竟

工作尽管拧螺丝,考试还得背八股。

简略来说,PHP 是通过一个 zval 的构造体实现的,源代码如下:
注:此源码是 PHP5 版本,7 及以上版本,请参考官网

struct _zval_struct {
     union {
          long lval;
          double dval;
          struct {
               char *val;
               int len;
          } str;
          HashTable *ht;
          zend_object_value obj;
          zend_ast *ast;
     } value;
     zend_uint refcount__gc;
     zend_uchar type;
     zend_uchar is_ref__gc;
};

这个 zval 构造体中的 type 字段, 代表变量以后的类型值, 常见的可能选项是 IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT 等;
弱类型,就是通过 type 字段的值, 取对应的 value 值来实现的;

这个 value 是联合体(union):

  • 如果 type 是 IS_STRING, 就取 value.str 的值;
  • 如果 type 是 IS_LONG, 就用 value.lval 的值;

联合体的特点在于,容许在雷同的内存地位存储不同的数据类型,也就是共享内存地址,其长度等于最长的子成员长度;
但同一时刻只能有一个成员带有有效值,比方设置 value.str='haha', 此时获取 value.lval 就不会失去 haha; 这一特点可无效节俭内存占用。

而从源码中的 _zval_struct 定义,咱们也能够看出:

每次批改变量为不同类型的值,只需更改 type,并赋值给与其对应的 value 联合体即可;

这就是 PHP 的弱类型变量实现的基本原理,只管源码是老旧的 PHP5 版本,但原理基本一致。

对于 PHP 的垃圾回收

PHP 是通过援用计数来做根本的垃圾回收的, 就是下面 zval 构造体中的 refcount__gc 字段, 其含意是变量的援用数目;

这个内存回收算法是赫赫有名的 Reference Counting,个别译为 援用计数,其算法思维十分简洁:

为每个内存对象调配一个计数器,当一个内存对象建设时计数器初始化为 1(本身援用本身),之后每有一个新变量援用此内存对象,则计数器加 1;
每当缩小一个对此内存对象的援用,则计数器减 1,如果缩小后值变为 0,则销毁并回收其占用的内存;

在 PHP 中,内存对象就是 zval,计数器就是 refcount__gc。
对于字段名这里补充一下,5.3 版本以前, 叫做 refcount, 5.3 及后续版本,引入新的垃圾回收算法来凑合循环援用计数后, 改为当初的名字。

当然了,下面只是最根本的 GC 思维,具体实现过程要简单的多,思维简洁不代表实现简略

垃圾是如何产生的

垃圾产生的原理其实很简略,有限循环;
失常的代码逻辑应该是个有向无环的图,最终都会有一个退出标记,如果一个变量将援用指向本人,就会产生相似 while(true) 的有限循环逻辑,从而导致垃圾的产生,看如下代码:

$a = [1];
$a[] = &$a;
unset($a);

unset($a)之后因为数组中有子元素指向 $a,所以refcount=1,但$a 曾经曾经没有任何内部援用了,这种状况是无奈通过失常的 gc 机制回收的,就会呈现所谓的 内存透露;不过这类垃圾只会呈现在 array、object 两种简单类型中;
这类代码如果只用于动静页面脚本,引起的泄露兴许不是很要紧,因为动静页面脚本的生命周期很短,PHP 会在脚本执行结束后,开释其所有资源;
但如果将 PHP 用到自动化脚本工作或是 deamon 过程,那么通过短暂循环后所积攒下来的内存泄露就会很重大,因为透露是线性增长的;
当然这也都是老黄历了,在 5.3 版本当前更新了垃圾回收机制,可将透露管制在某个阈值以下。

垃圾回收的基本原理

垃圾回收的 2 种状况:

  • 1、如果一个变量的 refcount 缩小之后变为 0,那么此 zval 可被开释,属优质垃圾,无需再进行多余操作;
  • 2、如果一个变量的 refcount 缩小之后仍大于 0,那么此 zval 还不能被开释,但它可能成为一个垃圾,需进一步操作;

针对第一种状况 GC 机制不会解决,只有第二种状况 GC 才会将变量收集起来,放入一个用于检测是否为垃圾的 buffer 链表,做模仿删除测试,测试逻辑如下:

  • 1、从 buffer 链表的 roots 开始 深度优先遍历,将遍历到的成员的 refcount 减 1;
  • 2、反复遍历,查看以后变量的援用是否为 0,为 0 则示意的确是能够销毁的垃圾,同时标记好以备删除; 如果 不为 0 则排除了援用全副来自本身成员的可能,即还有来自内部的援用,不是垃圾;
  • 3、因为步骤 (1) 对成员进行了 refcount 减 1 操作,此时须要再还原回去,对所有 refcount 加 1;
  • 4、再次遍历 buffer,将没有被标记删除的节点从链表中删除,最终剩下真正的垃圾,最初将这些垃圾革除。

这个算法的原理也很简介,如果垃圾是由成员援用本身导致的,那么对所有成员的援用次数执行 减一 操作后,变量自身的refcount 应该会变为0

对于垃圾回收原理的集体解析
1、如果如果一个变量的refcount 放弃不变或仍在增长,它必定不是一个垃圾,阐明还在应用;

2、如果 refcount 开始缩小,则至多阐明它是个准垃圾,如果缩小后变为 0,可释怀删除;如果仍大于 0,放到准垃圾回收池进行计算;

3、检测逻辑为何只做减 1 操作,如果 $a[] = &$a 这类代码被执行了 2 次,或者 N 次,那岂不是检测不进去了?比方:

$a = [1];
$a[] = &$a;
$a[] = &$a;
unset($a);

非也,非也,留神下面的遍历形式,是 DFS- 深度优先,以$a[] = &$a 被执行 2 次为例:
如果 $a 被断定为准垃圾,阐明执行过 unset($a) 操作,所以此时 $arefcount = 2;
DFS会按 0,1,2 的下标程序进行遍历:
对于 $a[0]执行 refcount-1 不会影响 $arefcount
对于 $a[1]执行 refcount-1 后,$arefcount= 2-1 =1
对于 $a[2]执行 refcount-1 后,$arefcount= 1-1 =0
模仿删除完结,$a变量的模仿后果refcount = 0,可销毁。

4、为何是将透露管制在某个阈值以下?
因为这类操作无奈防止,所以透露仍旧会存在;
增加了 buffer 待删除检测区后,能够肯定水平上缩小透露的持续增长,这个 buffer 的默认值是 10000 个变量,等凑满了才会进行解决,所以,这个机制会带来两个问题:

  • 1、达到足够量级才会解决;
  • 2、每次可解决的量级有下限;

因而,仍旧会有透露产生,但只有达到阈值,就会被解决掉:
但,如果有透露产生,那么内存的占用会是个折线图,忽高忽低,虽有透露,但可控;
而,旧版的垃圾回收机制,会导致透露是个线性增长的斜线图,透露没有下限,不可控。

7 版本当前的一些变动
其实从下面咱们也能够看到,这类垃圾只有简单类型才会呈现,若变量是 int 类型,是不会呈现援用本身的,所以简略类型其实不须要做啥计数;
因而 PHP7 开始,对于在 zval 的 value 字段中能保留下的值, 比方 IS_LONG、IS_DOUBLE 等简略类型,不再进行援用计数, 而是在拷贝时间接赋值, 这样就省掉了大量的援用计数相干的操作;
同样,对于那种基本没有值的类型, 比方,IS_NULL IS_FALSE IS_TRUE, 也不再援用计数了;
同时,PHP7 给变量增加了 type_flag,只有属于IS_TYPE_COLLECTABLE 的变量才会被 GC 收集,比方 array,object等简单类型。

对于 PHP 的 copy on write(写时复制) 和 change on write(写时扭转)

再看一遍最开始贴的 c 源码中的 zval 构造体,还有个 is_ref__gc 字段没讲它的用处,这波及 PHP 的 change on write 机制;
is_ref__gc 是一个标记位,示意 PHP 中的一个变量是否是 & 援用类型;
先来看一段代码:

$var = "var_str";
$var_dup = $var;
$var = 1;

代码执行后,$var_dup 的值还是 var_str, 这就是通过 copy on write 机制实现的。
PHP 在批改一个变量以前,会先查看这个变量的 refcount,如果 refcount 大于 1,PHP 就会执行拆散;
对于下面的代码,当执行到第三行时,因为 $var 指向的 zval 的 refcount 大于 1,此时会复制一个新的 zval 进去,将原 zval 的 refcount 减 1,并批改 symbol_table(全局符号表,存储了所有的变量符号);
注:PHP 通过此表存储变量符号,且每个简单类型如数组都有本人的符号表,因而 $a 和 $a[0] 尽管是两个符号,但 $a 存在全局符号表,$a[0] 则存在数组自身的符号表
如果同时执行如下调试代码:

debug_zval_dump($var);
debug_zval_dump($var_dup);

会看到这俩变量的 refcount 都是 2(debug_zval_dump执行时也会导致 refcount+1),也就是这俩变量曾经做了拆散。
注:本段测试代码需应用 5 版本,7 版本当前,简略类型不再计数,所以看不到 refcount,起因见下面的垃圾回收解释

因为 PHP 中,执行变量复制的时候,PHP 外部并不是真正的复制,而是采纳指向雷同的构造来节约开销,相似 shallow copy(浅拷贝
),当源变量发生变化时,再执行深拷贝。

同理,如果一个变量是援用类型,判断形式就会发生变化,参考如下代码:

$var = "var_str";
$var_ref = &$var;
$var_ref = 1;

这段代码完结当前,$var 也会被间接的批改为 1,这个过程唤作 change on write(写时扭转)。
当下面代码的第二行执行后,除了 $var 变量的 refcount+ 1 变为 2 外,其 is_ref__gc 也会被设置为 1,代表以后变量被援用了;
执行到第三行时,PHP 会查看 is_ref 字段,如果为 1,则不执行拆散,而是间接将 源变量 $var 的值批改为 1。

而如果将下面的代码改为:

$var = "var_str";
$var_dup = $var;
$var_ref = &$var;
$var_ref = 1;

同时存在 copy on write 和 change on write,执行逻辑如下:
第二行执行时,和后面讲的一样,$var_dup 和 $var 指向雷同的 zval,refcount 为 2;
第三行执行时,PHP 发现 refcount 大于 1,则先执行拆散操作, 将 $var_dup 拆散进来,再将 $var 和 $var_ref 做 change on write 关联,也就是,refcount=2, is_ref=1;

再次强调:

以上测试代码需应用 5 版本,7 版本当前因为简略类型不再计数,所以无奈查问其 refcount,起因参考后面的垃圾回收相干


本文内容参考并整顿自如下文章:

官网:https://www.php.net/manual/zh/features.gc.refcounting-basics.php(援用计数基本知识)
鸟哥:https://www.laruence.com/2008/09/19/520.html(变量拆散 / 援用)
以及:

// 鸟哥:// 深刻了解 PHP7 内核之 zval: https://www.laruence.com/2018/04/08/3170.html
// 深刻了解 PHP 原理之变量: https://www.laruence.com/2008/08/22/412.html

// 盘古大叔解析垃圾回收: https://github.com/pangudashu/php7-internal/blob/master/5/gc.md

//PHP5 中的 GC 算法演变:https://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html

正文完
 0