PHP的GC垃圾收罗体制

复合类型(compound types)

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

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

以上代码输出:

a: (refcount=1, is_ref=0)=array ('meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42)

这三个zval变量容器是:a,meaning,number.增加和减少refcount的规则和上面提到的一样特例,添加数组本身作为数组元素时:

<?php 
  $a = array('one'); 

  $a[] = &$a; 

  xdebug_debug_zval('a'); 
  ?>

以上代码输出的结果:

a: (refcount=2, is_ref=1)=array (0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=...)

可以看到数组a和数组本身元素a[1]指向的变量容器refcount为2

当对数组$a调用unset函数时,$a的refcount变为1,发生了内存泄漏
清理变量容器的问题。

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

__destruct /unset

        __destruct() 析构函数,是在垃圾对象被回收时执行。

        unset 销毁的是指向对象的变量,而不是这个对象。

ylg娱乐官网 1

引用计数基本知识

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

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

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

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

在上例中,新的变量是a,是在当前作用域中生成的.并且生成了类型为string和值为”new
string”的变量容器.在额外的两个字节信息中,”is_ref”被默认设置为false,因为没有任何自定义的引用生成.”refcount”被设定为1,因为这里只有一个变量使用这个变量容器.调用xdebug查看一下变量内容:

<?php 
  $a = "new string"; 
  xdebug_debug_zval('a'); 
  ?>

以上代码会输出:

a: (refcount=1, is_ref=0)='new string'

对变量a增加一个引用计数

<?php 
  $a = "new string"; 
  $b = $a; 
  xdebug_debug_zval('a'); 
  ?>

以上代码会输出:

a: (refcount=2, is_ref=0)='new string'

这时,引用次数是2,因为同一变量容器被变量a和变量b关联.当没必要时,php不会去复制已生成的变量容器.变量容器在”refcount”变成0时就被销毁.当任何关联到某个变量容易的变量离开它的作用域(比如:函数执行结束),或者对变量调用了unset()函数,”refcount”就会减1,下面例子就能说明:

<?php 
  $a = "new string"; 
  $b = $c = $a; 
  xdebug_debug_zval('a'); 
  unset($b, $c); 
  xdebug_debug_zval('a'); 
  ?>

以上代码会输出:

a: (refcount=3, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'

如果我们现在执行unset($a),$包含的类型和值的这个容器就会从内存删除

为解决这种垃圾,产生了新的GC

   在PHP5.3版本中,使用了专门GC机制清理垃圾,在之前的版本中是没有专门的GC,那么垃圾产生的时候,没有办法清理,内存就白白浪费掉了。在PHP5.3源代码中多了以下文件:{PHPSRC}/Zend/zend_gc.h
{PHPSRC}/Zend/zend_gc.c,
这里就是新的GC的实现,我们先简单的介绍一下算法思路,然后再从源码的角度详细介绍引擎中如何实现这个算法的。

新的GC算法

    在较新的PHP手册中有简单的介绍新的GC使用的垃圾清理算法,这个算法名为 Concurrent
Cycle Collection in Reference Counted Systems ,
这里不详细介绍此算法,根据手册中的内容来先简单的介绍一下思路:

首先我们有几个基本的准则:

1:如果一个zval的refcount增加,那么此zval还在使用,不属于垃圾

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

3:如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾

只有在准则3下,GC才会把zval收集起来,然后通过新的算法来判断此zval是否为垃圾。那么如何判断这么一个变量是否为真正的垃圾呢?

简单的说,就是对此zval中的每个元素进行一次refcount减1操作,操作完成之后,如果zval的refcount=0,那么这个zval就是一个垃圾。这个原理咋看起来很简单,但是又不是那么容易理解,起初笔者也无法理解其含义,直到挖掘了源代码之后才算是了解。如果你现在不理解没有关系,后面会详细介绍,这里先把这算法的几个步骤描叙一下,首先引用手册中的一张图:

ylg娱乐官网 2

A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,此算法会先把所有前面准则3情况下的zval节点放入一个节点(root)缓冲区(root
buffer),并且将这些zval节点标记成紫色,同时算法必须确保每一个zval节点在缓冲区中之出现一次。当缓冲区被节点塞满的时候,GC才开始开始对缓冲区中的zval节点进行垃圾判断。

B:当缓冲区满了之后,算法以深度优先对每一个节点所包含的zval进行减1操作,为了确保不会对同一个zval的refcount重复执行减1操作,一旦zval的refcount减1之后会将zval标记成灰色。需要强调的是,这个步骤中,起初节点zval本身不做减1操作,但是如果节点zval中包含的zval又指向了节点zval(环形引用),那么这个时候需要对节点zval进行减1操作。

C:算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成白色(代表垃圾),如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作,同时将这些zval的颜色变成黑色(zval的默认颜色属性)

D:遍历zval节点,将C中标记成白色的节点zval释放掉。

这ABCD四个过程是手册中对这个算法的介绍,这还不是那么容易理解其中的原理,这个算法到底是个什么意思呢?我自己的理解是这样的:

        比如还是前面那个变成垃圾的数组$a对应的zval,命名为zval_a, 
如果没有执行unset, zval_a的refcount为2,分别由$a和$a中的索引1指向这个zval。  

        用算法对这个数组中的所有元素(索引0和索引1)的zval的refcount进行减1操作,由于索引1对应的就是zval_a,所以这个时候zval_a的refcount应该变成了1,这样zval_a就不是一个垃圾。

        如果执行了unset操作,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用算法对数组中的所有元素(索引0和索引1)的zval的refcount进行减1操作,这样zval_a的refcount就会变成0,于是就发现zval_a是一个垃圾了。
算法就这样发现了顽固的垃圾数据。

举了这个例子,读者大概应该能够知道其中的端倪:

对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。

这个道理其实很简单,假设数组a的refcount等于m,
a中有n个元素又指向a,如果m等于n,那么算法的结果是m减n,m-n=0,那么a就是垃圾,如果m>n,那么算法的结果m-n>0,所以a就不是垃圾了

m=n代表什么? 
代表a的refcount都来自数组a自身包含的zval元素,代表a之外没有任何变量指向它,代表用户代码空间中无法再访问到a所对应的zval,代表a是泄漏的内存,因此GC将a这个垃圾回收了。

PHP中运用新的GC的算法

在PHP中,GC默认是开启的,你可以通过ini文件中的zend.enable_gc
项来开启或则关闭GC。当GC开启的时候,垃圾分析算法将在节点缓冲区(roots
buffer)满了之后启动。缓冲区默认可以放10,000个节点,当然你也可以通过修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 来改变这个数值,需要重新编译链接PHP。

当GC关闭的时候,垃圾分析算法就不会运行,但是相关节点还会被放入节点缓冲区,这个时候如果缓冲区节点已经放满,那么新的节点就不会被记录下来,这些没有被记录下来的节点就永远也不会被垃圾分析算法分析。如果这些节点中有循环引用,那么有可能产生内存泄漏。

之所以在GC关闭的时候还要记录这些节点,是因为简单的记录这些节点比在每次产生节点的时候判断GC是否开启更快,另外GC是可以在脚本运行中开启的,所以记录下这些节点,在代码运行的某个时候如果又开启了GC,这些节点就能被分析算法分析。当然垃圾分析算法是一个比较耗时的操作。

   
在PHP代码中我们可以通过gc_enable()和gc_disable()函数来开启和关闭GC,也可以通过调用gc_collect_cycles()在节点缓冲区未满的情况下强制执行垃圾分析算法。这样用户就可以在程序的某些部分关闭或则开启GC,也可强制进行垃圾分析算法。

$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];   //这里直接拿官方示例

回收周期

5.3.0PHP使用了新的同步周期回收算法,来处理上面所说的内存泄漏问题

首先,我们先要建立一些基本规则:

如果一个引用计数增加,它将继续被使用,当然就不再垃圾中.如果引用技术减少到零,所在的变量容器将被清除(free).就是说,仅仅在引用计数减少到非零值时,才会产生垃圾周期(grabage
cycle).其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾。

ylg娱乐官网 3

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

顽固垃圾的产生过程

   
如果读者已经阅读了变量内部存储相关的内容,想必对refcount和isref这些变量内部的信息有了一定的了解。这里我们将结合手册中的一个例子来介绍垃圾的产生过程:

<?php

        $a = “new string”;

?>

在这么简单的一个代码中,$a变量内部存储信息为

        a: (refcount=1, is_ref=0)=’new string’

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

<?php

    $a = “new string”;

    $b = $a;

?>

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

    a,b: (refcount=2, is_ref=0)=’new string’

当我们用unset删除$b变量的时候,$b对应的zval的refcount会减少1

<?php

        $a = “new string”; //a: (refcount=1, is_ref=0)=’new string’

        $b = $a;                 //a,b: (refcount=2, is_ref=0)=’new
string’

        unset($b);              //a: (refcount=1, is_ref=0)=’new
string’

?>

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

<?php

        $a = array(‘meaning’ => ‘life’, ‘number’ => 42);

?>

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都遵循变量的引用和计数原则,用图来表示:

ylg娱乐官网 4

 下面在$a中添加一个元素,并将现有的一个元素的值赋给新的元素:

<?php

        $a = array(‘meaning’ => ‘life’, ‘number’ => 42);

永利官网ylg客户端,        $a[‘life’] = $a[‘meaning’];

?>

那么$a的内部存储为:

a: (refcount=1, is_ref=0)=array (

        ‘meaning’ => (refcount=2, is_ref=0)=’life’,

        ‘number’ => (refcount=1, is_ref=0)=42,

        ‘life’ => (refcount=2, is_ref=0)=’life’

)

其中的meaning元素和life元素之指向同一个zval的:

ylg娱乐官网 5

现在,如果我们试一下,将数组的引用赋值给数组中的一个元素,有意思的事情就发生了:

<?php 

        $a = array(‘one’);

ylg娱乐官网,        $a[] = &$a;

?>

这样$a数组就有两个元素,一个索引为0,值为字符one,另外一个索引为1,为$a自身的引用,内部存储如下:

a: (refcount=2, is_ref=1)=array (

        0 => (refcount=1, is_ref=0)=’one’,

        1 => (refcount=2, is_ref=1)=…

)

“…”表示1指向a自身,是一个环形引用:

ylg娱乐官网 6

这个时候我们对$a进行unset,那么$a会从符号表中删除,同时$a指向的zval的refcount减少1

<?php

        $a = array(‘one’);

        $a[] = &$a;

        unset($a);

?>

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

ylg娱乐官网 7

这样,这么一个zval就成为了一个真是意义的垃圾了,新的GC要做的工作就是清理这种垃圾。

  • 如果一个zval容器的refcount增加,说明有新的变量(符号)指向这个容器,那么这个容器当然不会是垃圾,它将被继续使用。
  • 如果一个zval容器的refcount减少到0了,那么说明没有变量(符号)指向这个容器,它就会被php引擎销毁。
  • 如果一个zval容易的refcount减少了,但是不是0,那么这个容器就有可能是垃圾,就会被垃圾回收机制所管理。

PHP的基本GC概念

PHP语言同其他语言一样,具有垃圾回收机制。那么今天我们要为大家讲解的内容就是关于PHP垃圾回收机制的相关问题。希望对大家有所帮助。

PHP strtotime应用经验之谈PHP memory_get_usage()管理内存PHP
unset全局变量运用问题详解PHP
unset()函数销毁变量教你快速实现PHP全站权限验证一、PHP
垃圾回收机制(Garbage Collector 简称GC)
在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP的GC垃圾处理机制,防止内存溢出。当一个PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。GC进程一般都跟着每起一个SESSION而开始运行的.gc目的是为了在session文件过期以后自动销毁删除这些文件.二、__destruct
/unset __destruct() 析构函数,是在垃圾对象被回收时执行。

unset 销毁的是指向对象的变量,而不是这个对象。三、 Session
与PHP垃圾回收机制由于PHP的工作机制,它并没有一个daemon线程来定期的扫描Session信息并判断其是否失效,当一个有效的请求发生时,PHP
会根据全局变量
session.gc_probability和session.gc_divisor的值,来决定是否启用一个GC。
在默认情况下,session.gc_probability=1, session.gc_divisor
=100也就是说有1%的可能性启动GC(也就是说100个请求中只有一个gc会伴随100个中的某个请求而启动).

PHP垃圾回收机制的工作就是扫描所有的Session信息,用当前时间减去session最后修改的时间,同session.gc_maxlifetime参数进行比较,如果生存时间超过gc_maxlifetime(默认24分钟),就将该session删除。

但是,如果你Web服务器有多个站点,多个站点时,GC处理session可能会出现意想不到的结果,原因就是:GC在工作时,并不会区分不同站点的session.那么这个时候怎么解决呢?

  • 修改session.save_path,或使用session_save_path()让每个站点的session保存到一个专用目录,
  • 提供GC的启动率,自然,PHP垃圾回收机制的启动率提高,系统的性能也会相应减低,不推荐。
  • 在代码中判断当前session的生存时间,利用session_destroy()删除。

Session 与 GC

        由于PHP的工作机制,它并没有一个daemon线程来定期的扫描Session
信息并判断其是否失效,当一个有效的请求发生时,PHP 会根据全局变量
session.gc_probability 和session.gc_divisor的值,来决定是否启用一个GC,
在默认情况下, session.gc_probability=1, session.gc_divisor =100
也就是说有1%的可能性启动GC(也就是说100个请求中只有一个gc会伴随100个中的某个请求而启动).

        GC
的工作就是扫描所有的Session信息,用当前时间减去session最后修改的时间,同session.gc_maxlifetime参数进行比较,如果生存时间超过gc_maxlifetime(默认24分钟)
,就将该session删除。

        但是,如果你Web服务器有多个站点,多个站点时,GC处理session可能会出现意想不到的结果,原因就是:GC在工作时,并不会区分不同站点的session.

这时会输出:

PHP 垃圾回收机制(Garbage Collector 简称GC)

       
在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP
的GC垃圾处理机制,防止内存溢出。

        当一个
PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。GC进程一般都跟着每起一个SESSION而开始运行的。

        GC目的是为了在session文件过期以后自动销毁删除这些文件.

  即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。

什么算垃圾

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

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

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

  对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。

那么这个时候怎么解决呢?

        1. 修改session.save_path,或使用session_save_path()
让每个站点的session保存到一个专用目录,

        2.
提供GC的启动率,自然,GC的启动率提高,系统的性能也会相应减低,不推荐。

        3.
在代码中判断当前session的生存时间,利用session_destroy()删除.

  怎么管理这些容器并判断哪些是垃圾呢?当发现某个容器有可能是垃圾时,这个容器会被放进一个内存缓冲区,当缓冲区满了时,就会进行垃圾回收算法来找出垃圾并销毁。这里具体的算法可以看看官方文档,我就用一个网友的总结来描述:

每一种语言都有自己的自动垃圾回收机制,让程序员不必过分关心程序内存分配,但是在OOP中,有些对象需要显式的销毁;防止程序执行内存溢出。

a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)

涉及到垃圾回收的知识点

1.unset函数

    unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refount是否到0了,以及gc算法判断。

2.= null 操作;

    a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。

3.脚本执行结束

    脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。

a: (refcount=1, is_ref=0)=array (
   0 => (refcount=1, is_ref=0)='1',
   1 => (refcount=1, is_ref=0)='2'
)

  首先看了一下官方手册,只有php5.3版本以后的才有了所谓的新的垃圾回收机制GC,那么以前是怎么干的呢?以前是基于引用计数的方式,这里就需要提一下引用计数的知识,官方手册里面说php的每个变量都是存在一个叫做zval的容器里面,这个容器不仅包含了这个变量的值和类型,还包含了另外两个重要的信息,“is_ref”和“refcount”,“is_ref”看名字就应该知道大概和引用相关,它是一个bool值,如果这个值是true那么代表这是一个引用变量,否则是普通变量。“refcount”指的是有多少个变量(符号)指向这个zval容器。

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

发表评论

电子邮件地址不会被公开。 必填项已用*标注