PHP 的垃圾回收机制(GC)

前言

本篇内容仅是内容整合,主要参考以下这些博客:
PHP进阶学习之垃圾回收机制详解
PHP ZVAL是什么意思?
PHP 垃圾回收机制详解

一、什么是垃圾回收机制

程序在创建对象或者数组等引用类型实体的时候,系统会在堆内存上为之分配一段内存区,用来保存这些对象,当这些对象永久地失去引用后,就会变成垃圾,等待系统垃圾回收机制进行回收。

垃圾回收机制(Garbage Collector 简称GC)是一种动态存储分配的方案。它会自动释放程序不再需要的已分配的内存块。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑。在现在的流行各种语言当中,垃圾回收机制是新一代语言所共有的特征,如Python、PHP、C#、Ruby等都使用了垃圾回收机制。

二、PHP的垃圾回收机制

PHP5 和 PHP7 的垃圾回收机制原理都是利用 引用计数,PHP5.3之后版本为了处理循环的引用内存泄漏问题,在引用计数基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以避免内存泄露。

引用计数:每个内存对象都分配一个 refcount计数器,当内存对象被变量引用时,refcount计数器+1;当变量引用撤掉后(执行unset()后),refcount计数器-1;当 refcount计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。

内存泄露:当数组或对象使用 & 引用自己时,对数组或对象进行 unset,那么数组或对象会从符号表中删除,同时 refcount 减少1,但是数组或对象之前指向的refcount变为1而不是0,因此不能被回收,这样产生了内存泄露。

例:

$a = array("hello");
$a[]= &$a;
xdebug_debug_zval('a');

// 输出:
a: (refcount=2, is_ref=1)=array (0 => (refcount=2, is_ref=0)='hello', 1 => (refcount=2, is_ref=1)=...)
# “…”表示1指向a自身,是一个环形引用

php5.3之后当数组或对象的指向计数(refcount)减少到非0,并且在符号表中没有任何符号映射该数组或对象时,才会产生垃圾周期,也就是说该结构体(struct 中的 zend_value)才会被放在垃圾缓冲区,被当做疑似垃圾看待,等到缓冲区达到临界值时,触发回收算法,对疑似垃圾的结构体进行遍历,将指向计数(refcount)减1,然后判断 refcount 是否为0,如果是的话就确认结构体为垃圾,最后进行销毁,释放内存空间。

PHP的Zval结构

在讲回收机制前我们需要了解一下 PHP 的 Zval 结构,Zval 是PHP中最重要的数据结构之一(另一个比较重要的数据结构是hash table),它包含了PHP中的变量值和类型的相关信息。

它是一个 struct 结构体,基本结构为:

struct _zval_struct {
    zvalue_value value;     /* 存储变量的值*/
    zend_uint refcount__gc;  /* 表示引用计数 */
    zend_uchar type;          /* 变量具体的类型 */
    zend_uchar is_ref__gc;    /* 表示是否为引用 */
};
typedef struct _zval_struct zval;
1、zval_value value

变量的实际值,具体来说是一个zvalue_value的联合体(union):

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {                    /* string */
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value,used for array */
    zend_object_value obj;      /* object */
} zvalue_value;
2、zend_uint refcount__gc

该值实际上是一个计数器,用来保存有多少变量(或者符号,symbols, 所有的符号都存在符号表(symble table)中, 不同的作用域使用不同的符号表,关于这一点,我们之后会论述)指向该zval。

在变量生成时,其refcount=1,典型的赋值操作如a=b会令zval的refcount加1,而unset操作会相应的减1。在PHP5.3之前,使用引用计数的机制来实现GC,如果一个zval的 refcount较少到0,那么Zend引擎会认为没有任何变量指向该zval,因此会释放该zval所占的内存空间。

但,事情有时并不会那么简单。后面 我们会看到,单纯的引用计数机制无法GC掉循环引用的zval,即使指向该zval的变量已经被unset,从而导致了内存泄露(Memory Leak)。

3、zend_uchar type

该字段用于表明变量的实际类型。在开始学习PHP的时候,我们已经知道,PHP中的变量包括四种标量类(bool,int,float,string),两种复合类型(array, object)和两种特殊的类型(resource 和NULL)。

在zend内部,这些类型对应于下面的宏(代码位置 phpsrc/Zend/zend.h):

#define IS_NULL     0
#define IS_LONG     1
#define IS_DOUBLE   2
#define IS_BOOL     3
#define IS_ARRAY    4
#define IS_OBJECT   5
#define IS_STRING   6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_ARRAY   9
#define IS_CALLABLE 10
4、is_ref__gc

这个字段用于标记变量是否是引用变量。对于普通的变量,该值为0,而对于引用型的变量,该值为1。这个变量会影响zval的共享、分离等。关于这点,我们之后会有论述。

正如名字所示,ref_count__gc和is_ref__gc是PHP的GC机制所需的很重要的两个字段,这两个字段的值,可以通过xdebug等调试工具查看。

PHP的垃圾回收机制经理过3个不同的时期:5.2及之前版本、5.3-5.6、7.0之后。下面分别详细讲讲:

PHP5.2及之前版本

PHP5.2及之前版本的垃圾回收机制就是单纯的 引用计数。并且PHP在一个生命周期结束后就会释放此进程/线程所占的内容,这种方式决定了PHP在前期不需要过多考虑内存的泄露问题。

但是当两个或多个对象互相引用形成循环后,内存对象的 refcount计数器则不会消减为0;这时候,这一组内存对象已经没用了,但是不能回收,从而导致内存泄露的现象。

PHP5.3-5.6版本

php5.3开始,使用了新的垃圾回收机制,在 引用计数 基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以避免内存泄露。

在这些版本中,PHP把那些可能是垃圾的变量容器放入根缓冲区,当根缓冲区满了之后就会启动新的垃圾回收机制。

  • 如果发现一个zval容器中的refcount在增加,说明不是垃圾;
  • 如果发现一个zval容器中的refcount在减少,如果减到了0,直接当做垃圾回收;
  • 如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象;
  • 当缓冲区达到临界值,PHP会自动调用一个方法取遍历每一个值,如果发现是垃圾就清理。

在垃圾回收中,判断一个容器是否是真的垃圾,其思路并不复杂:如果一个变量容器的refcount的值全部来自其引用自身,那么它是一个垃圾。
具体操作是对变量及其成员的refcount进行模拟删除,即减1的操作,如果像我们上面说的,如果它的refcount值都来自其成员引用自身,那么模拟删除后,它的refcount值就会变成0,因此我们可以断定它是一个垃圾,要进行回收。

$a = array('hello');
xdebug_debug_zval( 'a' );
echo "<br>";
$a[] = &$a;
xdebug_debug_zval('a');

a: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='hello')
a: (refcount=2, is_ref=1)=array (0 => (refcount=2, is_ref=0)='hello', 1 => (refcount=2, is_ref=1)=...)
# “…”表示1指向a自身,是一个环形引用

可以看到数组a和数组本身元素a[1]指向的变量容器refcount为2。当对数组$a调用unset函数时,$a的refcount变为1,发生了内存泄漏。

尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素"1"仍然指向数组本身,所以这个容器不能被消除。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php将在请求结束时清除这个数据结构,但是php清除前,将耗费不少内存空间。

PHP7.0之后版本

注意:从PHP7的NTS版本开始,以下例程的引用将不再被计数,即 $c=$b=$a 之后 a 的引用计数也是1。
具体分类如下:

在PHP 7中,zval可以被引用计数或不被引用。在zval结构中有一个标志确定了这一点。

  • 对于null,bool,int和double的类型变量,refcount永远不会计数;
  • 对于对象、资源类型,refcount计数和php5的一致
  • 对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的);
  • 对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组。再次,这些生活在共享内存,因此不能使用refcounting。

 
注:Opcache是一种通过将解析的PHP脚本预编译的字节码(Operate Code)存放在共享内存中来避免每次加载和解析PHP脚本的开销,解析器可以直接从共享内存读取已经缓存的字节码(Operate Code),从而大大提高PHP的执行效率。

如下:

<?php
echo '测试字符串引用计数';
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
unset( $b);
xdebug_debug_zval( 'a' );
$b = &$a;
xdebug_debug_zval( 'a' );
// 输出:
测试字符串引用计数
a: (refcount=1, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
a: (refcount=2, is_ref=1)='new string'
# 字符串引用,计数不变,&地址引用会变


echo '测试数组引用计数';
$c = array('a','b');
xdebug_debug_zval( 'c' );
$d = $c;
xdebug_debug_zval( 'c' );
$c[2]='c';
xdebug_debug_zval( 'c' );
// 输出:
测试数组引用计数
c: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=3, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b', 2 => (refcount=1, is_ref=0)='c')
# 数组引用,数组本身计数加一,但数组里的键值对不变


echo '测试int型计数';
$e = 1;
xdebug_debug_zval( 'e' );
// 输出:
测试int型计数
e: (refcount=0, is_ref=0)=1
# int型不计数

三、回收周期

1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),

  • 每次对某个zval减1后就对其标记为“已减”。
     

2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。
 
3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。
 

需记住PHP5.3之后的垃圾回收算法有以下几点特性:

  • 1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。
  • 2、可以解决循环引用问题。
  • 3、可以总将内存泄露保持在一个阈值以下。

四、总结

个人理解: PHP是脚本语言,当脚本执行结束之后,脚本内使用的所有内存都会被释放,在执行过程中调用一个函数,当函数运行完毕后,就得赶紧释放掉这块儿内存以供给其他进程使用,而不是非得等到脚本执行结束后才销毁。当脚本执行结束后会把refcount 大于0的 zend_value 放入垃圾缓冲区,当缓冲区达到临界值时,触发回收算法,对疑似垃圾的结构体进行遍历,将指向计数(refcount)减1,然后判断 refcount 是否为0,如果是的话就确认结构体为垃圾,最后进行销毁,释放内存空间。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值