php中垃圾回收机制

我们可能在开发中经常会听到gc,是的gc就是垃圾回收容器,全称Garbage Collection。

此篇文章中“垃圾”的概念:如果一个变量容器能被减少到0,说明他就已经没有被引用了,属于正常销毁,所以不属于垃圾,而垃圾是指当外部引用被全部清除后,引用计数还不为0的变量容器

引用计数基本知识

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

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

如果安装了xdebug,可以通过调用函数 xdebug_debug_zval()显示"refcount"和"is_ref"的值。

<?php
$a = "new string";
xdebug_debug_zval('a');
?>
a: (refcount=1, is_ref=0)='new string'

把一个变量赋值给另一变量将增加引用次数(refcount).

$a = ['name'=>'lq','number'=>3];  //创建一个变量容器,变量a指向给变量容器,a的ref_count为1
$b = $a;//变量b也指向变量a指向的变量容器,a和b的ref_count为2
xdebug_debug_zval('a', 'b'); 
$b['name'] = '我的的技术成长之路';//变量b的其中一个元素发生改变,此时会复制出一个新的变量容器,变量b重新指向新的变量容器,a和b的ref_count变成1
xdebug_debug_zval('a', 'b'); 

以上将会输出:

a: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3) 
b: (refcount=2, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)
a: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)
b: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='lq', 'number' => (refcount=1, is_ref=0)=3)
所以,当变量a赋值给变量b的时候,并没有立刻生成一个新的变量容器,而是将变量b指向了变量a指向的变量容器,即内存"共享";而当变量b其中一个元素发生改变时,才会真正发生变量容器复制,这就是写时复制技术

因为同一个变量容器被变量 a 和变量 b关联。函数执行结束,或者对变量调用了函数 unset()时,”refcount“就会减1,下面的例子就能说明:

$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );

引用计数清0

当变量容器的ref_count计数清0时,表示该变量容器就会被销毁,实现了内存回收,这也是php5.3版本之前的垃圾回收机制

以上将会输出:

a: (refcount=3, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string' // 说明:unset并非一定会释放内存,当有两个或以上的变量指向的时候,并非会释放变量占用的内存,只是refcount减1.

当我们使用引用赋值时:

$name = "一路向北";
$temp_name = &$name;
xdebug_debug_zval('name');

输出结果:

name:(refcount=2, is_ref=1),string '一路向北' (length=18)

是的引用赋值会导致zval通过is_ref来标记是否存在引用的情况。

数组类型:

$name = ['a'=>'apple', 'b'=>'big_apple'];
xdebug_debug_zval('name');

我们会得到:

name:
(refcount=1, is_ref=0),
array (size=2)
'a' => (refcount=1, is_ref=0),string 'apple' (length=9)
'b' => (refcount=1, is_ref=0),string 'big_apple' (length=9)

这个结构应该也很好理解:对于数组来说是一个整体,对于数组中的k=>v来说也是一个独立的整体,各自维护自己的zval的refount和is_ref。

php的内存管理机制

//获取内存方法,加上true返回实际内存,不加则返回表现内存
var_dump(memory_get_usage());
$name = "一路向北";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());
int 1593048
int 1593264
int 1593048

过程:定义变量->增加内存->变量清除->恢复内存

看个例子:

$name = str_repeat('1',255);     //产生由255个1组成的字符串
$memory = memory_get_usage(); //获取当前占用内存
unset($name);
$memory_s = memory_get_usage(); //unset()后再查看当前占用内存
echo $memory -$memory_s ; //最后输出unset()之前占用内存减去unset()之后占用内存,
//如果是正数那么说明unset($name)已经将$name从内存中销毁(或者 说,unset()之后内存占用减少了)
//可是得到的结果是:-48,这是否可以说明unset($name)并没有起 到销毁变量$name所占用内存的作用
$name = str_repeat('1',256);   //产生由256个1组成的字符串
$memory = memory_get_usage(); //获取当前占用内存 unset($name);
$memory_s= memory_get_usage(); //unset()后再查看当前占用内存
echo $memory-$memory_s;
这个例子和上面的例子几乎相同,唯一的不同是,$name由256个1组成,即比第一个例子多了一个1
得到结果是:224。这是否可以说明,unset($name)已经将$name所占用的内存销毁了
$name = str_repeat('1',256);       //这和第二个例子完全相同
$php = &$name;
$memory = memory_get_usage();
unset($name); //销毁$name
$memory_s = memory_get_usage();
echo $php . '<br />';
echo $memory-$memory_s; /**
* 我们看到第一行有256个1,第二行是-48,按理说我们已经销毁了$name,*而$php只是引用$name的变量
* 应该是没有内容了,另外,unset($name)后内存占用却比unset()前增加了
*/
$name = str_repeat('1', 256);      //这和第二个例子完全相同
$php = &$name;
$memory = memory_get_usage();
unset($name); //销毁$name
unset($php); //销毁$php
$memory_s= memory_get_usage();
echo $php . '<br />';
echo$memory-$memory_s; /**
* 我们将$name和$php都使用unset()销毁,这时再看内存占用量之差也是* * 224,说明这样也可以释放内存
*/

由此得到结论是:unset()函数只能在变量值占用内存空间超过256字节时才会释放内存空间,并且如果变量存在引用赋值,那么需要指向该存储单元的所以变量都销毁才会释放内存空间

老版本php中如何产生内存泄漏垃圾?

产生内存泄漏主要真凶:环形引用。

一个经典的场景:

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

debug的输出:

a:
(refcount=2, is_ref=1),
array (size=2)
0 => (refcount=1, is_ref=0),string 'one' (length=3)
1 => (refcount=2, is_ref=1),
&array< // 递归引用自身

这样 $a数组就有了两个元素,一个索引0,值为one字符串,另一个索引为1,为$a自身的引用。

借用一下官方的图:

接下来我们删掉$a:

$a = ['one'];
$a[] = &$a;
unset($a);

得到:

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

如果在小于php5.3的版本就会出现一个问题:$a已经不在符号表了,没有变量再指向此zval容器,用户已无法访问,但是由于数组的refcount变为1而不是0,导致此部分内存不能被回收从而产生了内存泄漏。

5.3版本以后php是如何处理垃圾内存的?

新的垃圾回收机制

php5.3版本之后引入根缓冲机制,即php启动时默认设置指定zval数量的根缓冲区(默认是10000),当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量(默认是10000)后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题

为解决环形引用导致的垃圾,产生了新的GC算法,遵守以下几个基本准则:

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

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

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

引用php官方手册的配图:

以下解释属于个人理解,如有问题或错误请评论区留言。

A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,算法会在满足准则3情况下的zval节点放入一个节点(root)缓冲区(root buffer)(自我理解:当php发现有存在循环引用的zval时,就会把其投入到根缓冲区),并且将这些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释放掉。

总结:

  1. unset函数:unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refcount是否到0了,以及gc算法判断。
  2. = null 操作;a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。
  3. 脚本执行结束;脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。
  4. 以php的引用计数机制为基础(php5.3以前只有该机制),同时使用根缓冲区机制,当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题(php5.3开始引入该机制)

php中垃圾回收机制的更多相关文章

  1. java中垃圾回收机制和引用类型

    在java中JDK1.2版本以后,对象的引用类型分为四种,从高到低依次为:强引用.软引用.弱引用.虚引用. ①强引用的特点:垃圾回收机制绝不会回收它,即使内存不足时,JVM宁愿抛出OutOfMemor ...

  2. 了解java中垃圾回收机制

    Java的垃圾回收机制是Java环境自带有的,它不像c语言的malloc申请空间后需要Free()函数来释放,而Java中的代码块中所申请的空间可在程序执行完成后自动释放,但是是有局限性的,代码块所占 ...

  3. python中垃圾回收机制

    Python垃圾回收机制详解   一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题.在Python中,如果一个对象的引用数为0,Python虚拟 ...

  4. JVM中垃圾回收机制如何判断是否死亡?详解引用计数法和可达性分析 !

    因为热爱,所以坚持. 文章下方有本文参考电子书和视频的下载地址哦~ 这节我们主要讲垃圾收集的一些基本概念,先了解垃圾收集是什么.然后触发条件是什么.最后虚拟机如何判断对象是否死亡. 一.前言   我们 ...

  5. java中垃圾回收机制中的引用计数法和可达性分析法(最详细)

    首先,我这是抄写过来的,写得真的很好很好,是我看过关于GC方面讲解最清楚明白的一篇.原文地址是:https://www.zhihu.com/question/21539353

  6. PHP垃圾回收机制

    一.引用计数基本知识 每个php变量存在一个叫"zval"的变量容器中,当一个变量被赋常量值时,就会生成一个zval变量容器.一个zval变量容器,除了包含变量的类型和值,还包括两 ...

  7. PHP-----浅谈垃圾回收机制

    前言 大多数编程语言都会有自身的垃圾回收机制,php也不例外.经常听很多人说gc,也就是垃圾回收器,全程为Garbage Collection. 在php5.3之前,是不包括垃圾回收机制的,也没有专门 ...

  8. 深入了解C#系列:谈谈C#中垃圾回收与内存管理机制

    今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...

  9. C#中垃圾回收与内存管理机制

    今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...

随机推荐

  1. Spring 事务注意事项

    使用事务注意事项 1,事务是程序运行如果没有错误,会自动提交事物,如果程序运行发生异常,则会自动回滚. 如果使用了try捕获异常时.一定要在catch里面手动回滚. 事务手动回滚代码 Transact ...

  2. Kitty-Cloud环境准备

    项目地址 https://github.com/yinjihuan/kitty-cloud 开发工具 开发工具目前对应的都是我本机的一些工具,大家可以根据自己平时的习惯选择对应的工具即可. 工具 说明 ...

  3. TCP/IP中的传输层协议TCP、UDP

    TCP提供可靠的通信传输,而UDP则常用于让广播和细节控制交给应用的通信传输. 传输层协议根据IP数据报判断最终的接收端应用程序. TCP/IP的众多应用协议大多以客户端/服务端的形式运行.客户端是请 ...

  4. 使用Dism命令对Win7镜像进行操作

    在操作前,我们需要下载Win7部署工具AIK和Win7原版镜像 ★镜像迅雷链接 ed2k://|file|cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408 ...

  5. STL之list函数解析

    STL之list函数解析 list是C++标准模版库(STL,Standard Template Library)中的部分内容.实际上,list容器就是一个双向链表,可以高效地进行插入删除元素. 使用 ...

  6. 万字长文带你入门Zookeeper!!!

    导读 文章首发于微信公众号[码猿技术专栏],原创不易,谢谢支持. Zookeeper 相信大家都听说过,最典型的使用就是作为服务注册中心.今天陈某带大家从零基础入门 Zookeeper,看了本文,你将 ...

  7. Linux下修改efi启动项

    Linux下有一个efibootmgr工具可以编辑efi启动项,十分方便,简单介绍如下 直接运行efibootmgr会显示出当前所有efi启动项,每个启动项前都有相应编号, 可以使用efibootmg ...

  8. Java第十五天,泛型

    一.定义 泛型是一种未知的数据类型,即当我们不知道该使用哪种数据类型的时候,可以使用泛型. 泛型的本质是为了  参数化 类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型) ...

  9. C语言 文件操作(一)

    #include<stdio.h> int main(){          FILE *fp = fopen("f:\\lanyue.txt","r&quo ...

  10. spark rdd元素println

    1.spark api主要分两种:转换操作和行动操作.如果在转化操作中println spark打印了 我也看不到. val result = sqlContext.sql(sql) val resu ...