嘿,你忘记写博客了~

盛年不重来,一日难再晨,及时宜自勉,岁月不待人....

PHP生命周期与垃圾回收机制

发布时间:2020-10-30编辑:windydeng浏览(777)评论览(0)

    作为一名PHP开发者,深入了解下底层的实现还是有必要的,今天就我个人学习聊聊PHP生命周期和垃圾回收机制。

        PHP生命周期

        php的运行模式有两种,CGI和CLI模式,但是他们的php工作原理都是一样的,都是作为一种SAPI运行。什么是SAPI?(Server Application Programming Interface)服务端应用编程接口。SAPi通过一些列钩子函数能够与外围数据交互。


        PHP的生命周期包含四个部分:分别为MINIT、RINIT、RSHUTDOWN、MSHUTDOWN


        MINIT:模块初始化阶段。初始化一些常量、扩展、类、资源的等所有被php用到的东西。


        RINIT: 请求初始化阶段。当一个页面请求发生时,在请求处理前都会经历的一个阶段。对于fpm而言,是在worker进程accept一个请求并读取、解析完请求数据后的一个阶段。在这个阶段内,SAPI层将控制权交给PHP层,PHP初始化本次请求执行脚本所需的环境变量。


        RSHUTDOWN:请求关闭阶段。请求处理完后就进入了结束阶段,PHP就会启动清理程序。这个阶段,将flush输出内容、发送http响应内容等,然后它会按顺序调用各个模块的RSHUTDOWN方法。 RSHUTDOWN用以清除程序运行时产生的符号表,也就是对每个变量调用unset函数。


        MSHUTDOWN:模块关闭阶段。该阶段在SAPI关闭时执行,与模块初始化阶段对应,主要是资源清理,PHP模块操作关闭,同时,将回调各扩展的module shutdown钩子函数。这是发生在所有请求都已经结束之后,例如关闭fpm的操作。(这个是对于CGI和CLI等SAPI,没有“下一个请求”,所以SAPI立刻开始关闭。)


        垃圾回收机制

        什么是垃圾?

        首先需要定义一下“垃圾”的概念,新的GC负责清理的垃圾是指变量的容器zval还存在,但是又没有任何变量名指向此zval。因此GC判断是否为垃圾的一个重要标准是有没有变量名指向变量容器zval。

              假设有一段PHP代码,使用了一个临时变量$tmp存储了一个字符串,在处理完字符串之后,$tmp变量对我们没有意义,但这个变量实际还存在,$tmp符号依然指向它所对应的zval,GC会认为PHP代码中可能还会使用到此变量,所以不会将其定义为垃圾。

              如果在PHP代码中使用完$tmp后,调用unset删除这个变量,那么$tmp是不是就成为一个垃圾了呢。很可惜,GC仍然不认为$tmp是一个垃圾,因为$tmp在unset之后,refcount_gc减少1变成了0(假设没有别的变量和$tmp指向相同的zval),这时GC会直接将$tmp对应的zval的内存空间释放,$tmp和其对应的zval就根本不存在了。此时的$tmp也不是新的GC所要对付的那种“垃圾”。那么新的GC究竟要对付什么样的垃圾呢,下面将生产一个这样的垃圾。

        

        顽固垃圾的产生过程

           接下来将结合手册中的一个例子来介绍垃圾的产生过程:

    <?php
        $a = "new string";?>123

    代码中,$a变量内部存储信息为

    a: (refcount_gc=1, is_ref_gc=0)='new string'1

    当把$a赋值给另外一个变量的时候,$a对应的zval的refcount_gc会加1

    <?php
        $a = "new string";    $b = $a;?>1234

    此时$a和$b变量对应的内部存储信息为

    a,b: (refcount_gc=2, is_ref=0)='new string'1

    当用unset删除$b变量时,$b对应的zval的refcount_gc会减1。

    <?php
        $a = "new string"; //a: (refcount_gc=1, is_ref_gc=0)='new string'
        $b = $a;           //a,b: (refcount_gc=2, is_ref=0)='new string'
        unset($b);         //a: (refcount_gc=1, is_ref=0)='new string'?>12345

    对于普通的变量来说,这一切很正常,但是在复合类型变量(数组和对象)中,会发生比较有意思的事情:

    <?php
        $a = array('meaning' => 'life', 'number' => 42);?>123

    a内部存储信息为:

    a: (refcount=1, is_ref=0)=array ( 
      'meaning' => (refcount=1, is_ref=0)='life', 
      'number' => (refcount=1, is_ref=0)=42)
    
    数组变量本身($a)在引擎内部实际上是一个哈希表,这张表中有两个zval项 meaning和number,
    所以实际上那一行代码中一共生成了3个zval,这3个zval都遵循变量的引用和计数原则
    这时对$a进行unset,那么$a会从符号表中删除,同时$a指向的zval的refcount_gc减少1.

    那么问题就产生了,$a已经不在符号表中,用户无法再访问此变量,但是$a之前指向的zval的refcount_gc变为1而不是0,因此不能被回收,从而产生内存泄露,新的GC要做的工作就是清理此类垃圾。

    下面简单的介绍一下算法思路,在较新的PHP手册中有简单的介绍新的GC使用的垃圾清理算法,这个算法名为 Concurrent Cycle Collection in Reference Counted Systems(引用计数系统中的同步周期回收)。

    首先有几个基本的准则:
    1:如果一个zval的refcount_gc增加,那么此zval还在使用,不属于垃圾
    2:如果一个zval的refcount减少到0, 那么zval可以被释放掉,不属于垃圾
    3:如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾

    算法介绍:
          A:为避免不得不检查所有引用计数可能减少的垃圾周期,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。

          B:模拟删除每个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减”1”,如果某个普通变量引用计数变成0了,就对这个普通变量再做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰。

          C:模拟恢复每个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。

    算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历即可(最典型的深搜遍历)。复杂度为执行模拟操作的节点数正相关,不只是紫色的那些疑似垃圾变量。



关键字词:PHP生命周期与垃圾回收机制