MENU

PHP的GC机制

December 6, 2021 • Read: 1274 • WEB Security Learning

0x00什么是GC机制

在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP 的GC垃圾处理机制,防止内存溢出。
当一个 PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。GC进程一般都跟着每起一个SESSION而开始运行的.gc目的是为了在session文件过期以后自动销毁删除这些文件.
php的垃圾回收机制主要有三个方面的知识

引用计数基本知识
回收周期(Collecting Cycles)
性能方面考虑的因素

0x01 Xdebug环境配置

首先得知道Xdebug是啥

Xdebug是一个开放源代码的PHP程序调试器(即一个Debug工具),可以用来跟踪,调试和分析PHP程序的运行状况。

配置环境是phpstudy+phpstorm+Xdebug
注意:这里强烈建议去虚拟机安装(因为我真机环境有点多,端口占用太多了,我直接原地去世,当时真机一晚上,虚拟机半小时
回到正题
首先准备一个phpstorm
支持官网(破解版很香,但不道德,所以自己去找
再准备一个phpstudy
下载地址
我安装的是 7.3 版本的Xdebug
下载地址

第一步打开phpstudy建立一个新的站点

然后高级设置中打开目录索引

然后我们直接看站点创建成功没有(前提是开了Apache和Mysql服务

成功后我们去这个网站根目录写一个index.php文件(等下访问没成功看看是不是文件隐藏后缀没改
文件内容为<?php phpinfo(); ?>

然后我们访问来看看成功没

然后重点来了噢 认真!
先去皮卡丘开一下php扩展Xdebug(只是让他配一下那个ini,等下可以少改一点,并不是用它本身的扩展

然后我们刚才下的Xdebug
改名为xdbug.dll 并且移动到ext文件夹下(其实都行,只是为了修改路径方便

然后我们刚才扩展开了 现在去php.ini中修改配置就行

[Xdebug]
zend_extension=C:/phpstudy_pro/Extensions/php/php7.3.4nts/ext/xdebug.dll //设置为我们下的Xdebug
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.auto_trace=1
xdebug.trace_output_dir=C:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.trace
xdebug.profiler_enable=Off
xdebug.profiler_output_dir=C:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.profiler
xdebug.remote_enable=1 //改成1,打开监听
xdebug.remote_host=debug  //这里我也改了
xdebug.remote_port=9100  //设置端口,默认9000,可以修改,但后面配置的端口要和这个一样
xdebug.remote_handler=dbgp
xdebug.idekey=PHPSTORM  //这个是没有的我们要加上

然后我们再去看看我们扩展设置好了吗 访问index.php

初步成功 然后来设置phpstorm
我们先打开debug这个目录

然后file->settings->php
大家配置自己的路径,配置后两个版本要是一样的

这里设置自己刚刚文件配置一样的端口,我上面是9100,所以这里也是9100

服务这里新建一个,并且配置网站域名和端口(就是我们在皮卡丘上的域名和端口

然后设置代理(设置为ini一样的就行

接下来是这里就是环境配置
新建这个页面

然后设置为这个

然后我们去开启监听就配好环境了

一步一步来,亲测有效
参考链接

0x02 引用计数

这里主要是php7.3的 现在网上大部分都是php5.3的

每个php变量存在一个叫做"zval"的变量容器中.一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息.第一个是"is_ref",是个bool值,用来标识这个变量是否是属于引用集合(reference set).通过这个字节,php引擎才能把普通变量和引用变量区分开.由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用.第二个额外字节是"refcount",用来表示指向这个zval变量容器的变量(也称符号即symbol)个数

我们先从底层来看看zval
zval 的结构

// php 变量对于的c结构体
struct _zval_struct {

    zend_value value;
    union {
       ……
    } u1;
    union {
        ……
    } u2;
};

u1 结构比较复杂,主要是用于识别变量类型
u2 这里面大多都是辅助字段,变量内部功能的实现、提升缓存友好性
zend_value
它也是结构体中内嵌的一个联合体

typedef union _zend_value {

    zend_long         lval;//整形

    double            dval;//浮点型

    zend_refcounted  *counted;//获取不同类型的gc头部

    zend_string      *str;//string字符串

    zend_array       *arr;//数组

    zend_object      *obj;//对象

    zend_resource    *res;//资源

    zend_reference   *ref;//是否是引用类型

   

    // 忽略下面的结构,与我们讨论无关

    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

在 zval的 value中就记录了引用计数zend_refcounted *counted这个类型,我们的垃圾回收机制也是基于此的。

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

所有的复杂类型的定义, 开始的时候都是zend_refcounted_h结构, 这个结构里除了引用计数以外, 还有GC相关的结构. 从而在做GC回收的时候, GC不需要关心具体类型是什么, 所有的它都可以当做zend_refcounted*结构来处理.

在php中 除了 array和object类型的变量,其余大部分是自动回收
php 普通变量的回收和 该变量的引用次数有关。
当一个变量被赋常量值时,就会生成一个zval变量容器,如下例所示:

$m1 = "whoami";

新的变量m1,是在当前作用域中生成的,并且生成类型为string和值为"whoami"的变量容器,额外的两个字节信息中,"is_ref"被默认设置为false,因为没有任何定义的引用生成。"refcount"未显示(默认为0,版本问题),因为这里只有一个变量使用这个容器,我们可以来用xdebug来查看一下这个变量的内容:

$m1 = "whoami";    
xdebug_debug_zval('m1');

以上代码的输出为

m1:(refcount, is_ref=0)='whoami'

这里的引用计数未显示
经过了解确实是版本问题
官方的php5.3版本

$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)=1
a:(refcount=1, is_ref=0)=10
a: no such symbol

而我的7.3版本结果为

a: (refcount=0, is_ref=0)=1
a: (refcount=0, is_ref=0)=10
a: no such symbol

具体原因出在
在 PHP7 中,为一个变量赋值的时候,包含了两部分操作:

1:为符号量(即变量名)申请一个 zval_struct 结构

2:将变量的值储存到 zval_struct.value 中 对于 zval 在 value 字段中能保存下的值,就不会在对他们进行引用计数,而是在拷贝的时候直接赋值, 这样就省掉了大量的引用计数相关的操作, 这部分类型有:

IS_LONG
IS_DOUBLE
当然对于那种根本没有值, 只有类型的类型, 也不需要引用计数了:
IS_NULL
IS_FALSE
IS_TRUE

在PHP7.x版本中,对于这些类型的数据是不走引用计数了的,在结果中还可以看出,对于变量的值传递方式赋值,使用了COW(copy on write)机制,对于普通的赋值,PHP直接将两个变量指向同一个内存地址,只有当其中一个值发生变化时,才会开辟新的内存空间用来存放改动变量的值
但是当我们主动使用引用传递引用计数毫无疑问会增加就像这样

$a = 'hello,world';
$b = &$a;
xdebug_debug_zval( 'a');
#结果 a: (refcount=2, is_ref=1)='hello,world'

在php7中
简单数据类型

整形(不使用引用计数)
浮点型(不使用引用计数)
布尔型(不使用引用计数)
NULL(不使用引用计数)

复杂数据类型

字符串

内部字符串(不使用引用计数,引用计数值恒为 0)
普通字符串(使用引用计数,初始值为 1)

数组

普通数组(使用引用计数,初始值为 1)
不可变数组(不使用引用计数,使用伪计数值 2)
对象(使用引用计数,初始值为 1)

拷贝机制

$a = 'hello';
$b = $a;//$a赋值给$b的时候,$a的值并没有真的复制了一份
echo xdebug_debug_zval( 'a');//$a的引用计数为0
$a = 'world';//当我们修改$a的值为123的时候,这个时候就不得已进行复制,避免$b的值和$a的一样
echo xdebug_debug_zval( 'a');///$a的引用计数为0

用这个例子也能体现出PHP的拷贝机制,其实,当你把 $a 赋值给 $b 的时候,$a 的值并没有真的复制了一份,这样是对内存的极度不尊重,也是对时间复杂度的极度不尊重,计算机仅仅是将 $b 指向了 $a 的值而已,这就叫多快好省。那么,什么时候真正的发生复制呢?就是当我们修改 $a 的值为 123 的时候,这个时候就不得已进行复制,避免 $b 的值和 $a 的一样。

0x03垃圾回收机制

当一个 zval 在被 unset 的时候、或者从一个函数中运行完毕出来(就是局部变量)的时候等等很多地方,都会产生 zval 与 zend_value 发生断开的行为,这个时候 zend 引擎需要检测的就是 zend_value 的 refcount 是否为 0,如果为 0,则直接 KO free 空出内容来。如果 zend_value 的 recount 不为 0,这个 value不能被释放,但是也不代表这个 zend_value 是清白的,因为此 zend_value 依然可能是个垃圾。

(1)当 php 变量 $a 的 refcount=0 时,变量 $a 就会被垃圾回收
(2)当 php 变量 $a 的 refcount>0 时,变量 $a 但也可能被认为是垃圾

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

这种情况下,zend_value 不会能释放,但也不能放过它,不然一定会产生内存泄漏,所以这会儿 zend_value 会被扔到一个叫做垃圾回收堆中,然后 zend 引擎会依次对垃圾回收堆中的这些 zend_value 进行二次检测,检测是不是由于上述两种情况造成的 refcount 为 1 但是自身却确实没有人再用了,如果一旦确定是上述两种情况造成的,那么就会将 zend_value 彻底抹掉释放内存。

垃圾回收发生在什么时候?
可能会有疑问,就是 php 不是运行一次就销毁了吗,我要 gc 有何用?并不是的,首先当一次 fpm 运行完毕后,最后一定还有 gc 的,这个销毁就是 gc;其次是,内存都是即用即释放的,而不是攒着非得到最后,你想想一个典型的场景,你的控制器里的某个方法里用了一个函数,函数需要一个巨大的数组参数,然后函数还需要修改这个巨大的数组参数,你们应该是函数的运行范围里面修改这个数组,所以此时会发生写时拷贝了,当函数运行完毕后,就得赶紧释放掉这块儿内存以供给其他进程使用,而不是非得等到本地 fpm request 彻底完成后才销毁。

(1)fpm 运行完毕后,最后一定会 gc 的
(2)运行过程中,也会 gc 的,内存都是即用即释放的,而不是攒着非得到最后 gc

参考链接1
参考链接2
参考链接3
参考链接4

Last Modified: February 12, 2022
Archives Tip
QR Code for this page
Tipping QR Code