zoukankan      html  css  js  c++  java
  • PHP垃圾回收机制(GC)

    PHP垃圾回收机制(GC)

         前言

          垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C, C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java, javaScript 和 PHP 可以自动管理内存。

          下面是阅读 《官方手册-垃圾回收机制》后,根据我自己的理解做的笔记。方便日后查阅。

          1、引用计数的基本知识

          我们要了解GC,那么首先要了解引起垃圾回收的计数是什么。

          在php中,每个变量存在一个叫“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括另外两个字节的额外信息:is_refrefcount

          当一个变量被赋常量值时,就会生成一个zval变量容器。

          1)is_ref

          is_ref是个bool值,用来标识这个变量是否是属于引用集合。通过这个字节,php引擎才能把普通变量和引用变量区分开来。由于php允许用户通过"&"来使用自定义的引用,所以zval中还有一个内部引用计数机制,来进行优化内存。

          2)refcount

          refcount用以表示指向这个zval变量容器的变量(也称符号即symbol)的个数。所有符号存在一个符号表当中,每个符号都有作用域。

         3)简单来讲:

          refcount就是多少个变量是一样的用了相同的值,那么refcount就是这个值。  

          is_ref就是当有变量用了&的形式进行赋值,那么is_ref的值就会增加1

          2、举例

          接下来的案例是以php5.6版本下运行的

          1)Example #1 

          当一个变量被赋常量值时,就会生成一个zval变量容器,如下例这样:

    1 Example #1 
    2 <?php
    3 $a = "new string";
    4 xdebug_debug_zval( 'a' );  //显示zval的信息
    5 
    6 输出:
    7 a: (refcount=1, is_ref=0)='new string' 
    8 ?>

          2)Example #2

          增加一个zval的引用计数

    1 <?php
    2 $a = "new string";
    3 $b = $a;
    4 xdebug_debug_zval( 'a' );
    5 
    6 输出:
    7 a: (refcount=2, is_ref=0)='new string'

          分析:这时,引用次数是2,因为同一个变量容器被变量 a 和变量 b关联.当没必要时,php不会去复制已生成的变量容器。

          3)Example #3

          减少引用计数

     1 <?php
     2 $a = "new string";
     3 $c = $b = $a;
     4 xdebug_debug_zval( 'a' );
     5 unset( $b, $c );
     6 xdebug_debug_zval( 'a' );
     7 
     8 输出:
     9 a: (refcount=3, is_ref=0)='new string'
    10 a: (refcount=1, is_ref=0)='new string'

          分析:当任何关联到某个变量容器的变量离开它的作用域(比如:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1。变量容器在”refcount“变成0时就被销毁。如果我们在上面的例子中继续执行 unset($a),包含类型和值的这个变量容器就会从内存中删除。

          4)Example #4

          当考虑像 array和object这样的复合类型时,事情就稍微有点复杂. 与 标量(scalar)类型的值不同,array和 object类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器

    1 <?php
    2 $a = array( 'meaning' => 'life', 'number' => 42 );
    3 xdebug_debug_zval( 'a' );
    4 
    5 输出:
    6 a: (refcount=1, is_ref=0)=array (
    7    'meaning' => (refcount=1, is_ref=0)='life',
    8    'number' => (refcount=1, is_ref=0)=42
    9 )

           图示:

           

           分析:这三个zval变量容器是: a,meaning和 number。增加和减少”refcount”的规则和上面提到的一样. 下面, 我们在数组中再添加一个元素,并且把它的值设为数组中已存在元素的值

           5)Example #5

           添加一个已经存在的元素到数组中 

     1 <?php
     2 $a = array( 'meaning' => 'life', 'number' => 42 );
     3 $a['life'] = $a['meaning'];
     4 xdebug_debug_zval( 'a' );
     5 
     6 输出:
     7 a: (refcount=1, is_ref=0)=array (
     8    'meaning' => (refcount=2, is_ref=0)='life',
     9    'number' => (refcount=1, is_ref=0)=42,
    10    'life' => (refcount=2, is_ref=0)='life'
    11 )

          图示:

         

           分析:从以上的xdebug输出信息,我们看到原有的数组元素和新添加的数组元素关联到同一个"refcount"2的zval变量容器. 尽管 Xdebug的输出显示两个值为'life'的 zval 变量容器,其实是同一个。

           6)Example #6

           从数组中删除一个元素

     1 <?php
     2 $a = array( 'meaning' => 'life', 'number' => 42 );
     3 $a['life'] = $a['meaning'];
     4 unset( $a['meaning'], $a['number'] );
     5 xdebug_debug_zval( 'a' );
     6 
     7 输出:
     8 a: (refcount=1, is_ref=0)=array (
     9    'life' => (refcount=1, is_ref=0)='life'
    10 )

             分析:删除数组中的一个元素,就是类似于从作用域中删除一个变量. 删除后,数组中的这个元素所在的容器的“refcount”值减少,同样,当“refcount”为0时,这个变量容器就从内存中被删除

             7)Example #7

             把数组作为一个元素添加到自己

             现在,当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,下个例子将说明这个。例中我们加入了引用操作符,否则php将生成一个复制。

     1 <?php
     2 $a = array( 'one' );
     3 $a[] =& $a;
     4 xdebug_debug_zval( 'a' );
     5 
     6 输出:
     7 a: (refcount=2, is_ref=1)=array (
     8    0 => (refcount=1, is_ref=0)='one',
     9    1 => (refcount=2, is_ref=1)=...
    10 )
    11 ?>

         

          分析:这样$a数组就有两个元素,一个索引为0,值为字符one,另外一个索引为1,为$a自身的引用。“…”说明发生了递归操作,意味着"..."指向原始数组。

          8)Example #8

    1 <?php
    2 $a = array( 'one' );
    3 $a[] =& $a;
    4 unset($a);
    5 xdebug_debug_zval( 'a' );
    6 
    7 输出:
    8 a: no such symbol
    9 ?>

          图示:

         

          分析: 那么问题也就产生了,$a已经不在符号表中了,用户无法再访问此变量,但是$a之前指向的zval的refcount变为1而不是0,因此不能被回收。

         清理变量容器的问题

          尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素“1”仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。

         GC算法

          1、为解决上面这种垃圾,产生了新的GC

          在PHP5.3版本中,使用了专门GC机制清理垃圾,在之前的版本中是没有专门的GC,php无法处理循环的引用内存泄露。那么垃圾产生的时候,没有办法清理,内存就白白浪费掉了。

          但是自5.3之后php使用引用计数系统中同步周期回收的同步算法,仅处理这个内存泄露问题。

          基本准则:

          1)如果一个zval的refcount增加,那么表明该变量的zval还在使用,不属于垃圾

          2)如果一个zval的refcount减少到0,那么zval可以被释放掉,可以清除,不属于垃圾

          3)如果在经过模拟删除后一个zval的refcount减1,如果该zval的引用次数为是大于0,那么此zval不能被释放,可能是一个垃圾

          4、垃圾回收周期(Collecting Cycles)

          

           回收的过程结合gc的结构一起来分析:

     1 #gc 的结构 zend_refcounted_h 具体如下:
     2 typedef struct _zend_refcounted_h {
     3     uint32_t         refcount; // 记录 zend_value 的引用数
     4     union {
     5         struct {
     6             zend_uchar    type,  // zend_value的类型, 与zval.u1.type一致
     7             zend_uchar    flags, 
     8             uint16_t      gc_info // GC信息,记录在 gc 池中的位置和颜色,垃圾回收的过程会用到
     9         } v;
    10         uint32_t type_info;
    11     } u;
    12 } zend_refcounted_h;

          1)步骤A

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

         底层实现:

         1. 如果当变量的 refcount 减小后大于 0,PHP 并不会立即对这个变量进行垃圾鉴定和回收,而是放入一个缓冲区中,等这个缓冲区满了以后 (默认是10000 个值) 再统一进行处理,加入缓冲区的是变量 zend_value 里的 gc,目前垃圾只会出现在数组和对象两种类型中,数组的情况上面已经介绍了,对象的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。

         2.一个变量只能加入一次缓冲区,为了防止重复加入,变量加入后会把 zend_refcounted_h.gc_info 置为 GC_PURPLE,即标为紫色,后续不会重复插入。

         2)步骤B

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

         底层实现:

         从缓冲区链表的 roots 开始遍历,把当前 value 标识为灰色 (zend_refcounted_h.gc_info 置为 GC_GREY),然后对当前 value 的成员进行深度优先遍历,把成员 value 的 refcount 减 1,并且也标为灰色

         3)步骤C

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

         底层实现:

         重复遍历缓冲区链表,检查当前 value 引用是否为 0,为 0 则表示确实是垃圾,把它标为蓝色 (GC_BLUE),如果不为 0 则排除了引用全部来自自身成员的可能,表示还有外部的引用,并不是垃圾,这时候因为步骤 (1) 对成员进行了 refcount 减 1 操作,需要再还原回去,对所有成员进行深度遍历,把成员 refcount 加 1,同时标为黑色

         4)步骤D

         将步骤C中蓝色的节点遍历出来删除掉。

         底层实现:

         再次遍历缓冲区链表,将非 GC_BLUE 的节点从 roots 链表中移出,最终 roots 链表中全部为真正的垃圾,最后将这些垃圾清除。

         5、GC算法与PHP的集成

         在php中垃圾回收机制默认是打开的,在你的php.ini中可以手动设置,通过zend.enable_gc这个属性进行开启或关闭垃圾回收机制。

         当开启了垃圾回收机制后,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区具有固定的大小,当然你可以通过修改php源码文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES来修改根缓存区的大小(注意修改后需要重新编译php)。

         当关闭垃圾回收机制后,这个循环查找算法将不会执行,然而可能根会一直存在于根缓冲区中,不管在配置中是否激活了垃圾回收机制。

         当然你也可以通过调用gc_enable()和gc_disable()函数来打开和关闭垃圾回收机制,效果和修改配置项相同。即使根缓冲区还没有满,也能强制执行周期回收。

          6、总结      

          1)PHP 5.3 版本之前

          不包括垃圾回收机制,也没有专门的垃圾回收器,实现垃圾回收就是简单判断一下变量的zval的refcount是否为0,是的话就释放。

          但是如果这么简单的判断垃圾回收的话,很容易引起程序过程中内存溢出。如果存在像2例子中的环形(循环)引用,这种"自身指向自身"的情况的话,那么变量将无法回收造成内存泄露,所以从php5.3开始就出现了专门负责清理垃圾数据防止内存泄露的垃圾回收器。

         2)在 5.3 版本之后,

         做了这些优化:

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

         PHP7与PHP5的GC机制不同的地方

         PHP7在垃圾回收机制上做了优化

         想要深入的理解这块,对于没有太多C语言基础的我来说有点难,因为这里面涉及到php底层C实现的过程,所以这里参考了下文章《PHP7垃圾回收机制浅析

    1 <?php
    2 $a = "new String";
    3 xdebug_debug_zval( 'a' );
    4 
    5 php5.6下输出
    6 a: (refcount=1, is_ref=0)='new string'
    7 
    8 php7.2下输出
    9 a: (interned, is_ref=0)='new String' //不使用引用计数的字符串类型被叫做“interned string(保留字符串)

        关于垃圾回收的小知识点

        1)unset():unset()只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了。

        2)null:将null赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0。

        3)脚本执行结束:该脚本中所有内存都会被释放,无论是否有环引用。

    参考链接:

    http://docs.php.net/manual/zh/features.gc.refcounting-basics.php

    https://www.jianshu.com/p/d73b3ca418b0

    https://learnku.com/articles/33451

     

     

  • 相关阅读:
    梯度算法之梯度上升和梯度下降
    如何用hexo+github搭建个人博客
    《机器学习实战-KNN》—如何在cmd命令提示符下运行numpy和matplotlib
    Python的operator.itemgetter函数和sorted函数
    源代码中直接package edu.princeton.cs.algs4还是import edu.princeton.cs.algs4问题
    关于在windows命令提示符cmd下运行Java程序的问题
    Windows10下用Anaconda3安装TensorFlow教程
    如何理解假设空间与版本空间?
    在windows64位Anaconda3环境下安装XGBoost
    用FastDFS一步步搭建图片服务器(单机版)
  • 原文地址:https://www.cnblogs.com/hld123/p/13385443.html
Copyright © 2011-2022 走看看