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. [vijos1159&洛谷1494]岳麓山上打水<迭代深搜>

    题目链接:https://vijos.org/p/1159 https://www.luogu.org/problem/show?pid=1494 这是今天的第三道迭代深搜的题,虽然都是迭代深搜的模板 ...

  2. Python——五分钟理解函数式编程与闭包

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第9篇文章,我们来聊聊Python的函数式编程与闭包. 函数式编程 函数式编程这个概念我们可能或多或少都听说过,刚听 ...

  3. Java实验五参考答案

    1.找错误 btOK.setOnAction( new EventHandler<ActionEvent> { public void handle (ActionEvent e) { S ...

  4. Java 程序该怎么优化?(工具篇)

    程序员:为什么程序总是那么慢?时间都花到哪里去了? 面试官:若你写的 Java 程序,出现了性能问题,该怎么去排查呢? 工欲善其事必先利其器,为你呈上一箩筐性能优化工具,必有一款满足你,废话不多说,直 ...

  5. 泛型代码中的 default 关键字

    在泛型类和泛型方法中会出现的一个问题是,如何把缺省值赋给参数化类型,此时无法预先知道以下两点: l        T将是值类型还是引用类型 l        如果T是值类型,那么T将是数值还是结构 对 ...

  6. linux安装常用软件和查询基本信息

                                                                          linux安装常用软件和查询基本信息 1. 安装常用软件 [ ...

  7. 一起了解 .Net Foundation 项目 No.23

    .Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. WorldWide Tel ...

  8. 基于 Spring Cloud 的微服务架构实践指南(下)

    show me the code and talk to me,做的出来更要说的明白 本文源码,请点击learnSpringCloud 我是布尔bl,你的支持是我分享的动力! 一.引入 上回 基于 S ...

  9. Linux bash篇(四 命令)

    1.一次执行多个命令        ; eg: ls -al ; touch data.txt 2.根据情况执行命令       &&     || cmd1 && c ...

  10. javascript入门 之 zTree(十四 增删查改)(一)

    <!DOCTYPE html> <HTML> <HEAD> <TITLE> ZTREE DEMO - beforeEditName / beforeRe ...