php中垃圾回收机制
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释放掉。
总结:
- unset函数:unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refcount是否到0了,以及gc算法判断。
- = null 操作;a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。
- 脚本执行结束;脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。
- 以php的引用计数机制为基础(php5.3以前只有该机制),同时使用根缓冲区机制,当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题(php5.3开始引入该机制)
php中垃圾回收机制的更多相关文章
- java中垃圾回收机制和引用类型
在java中JDK1.2版本以后,对象的引用类型分为四种,从高到低依次为:强引用.软引用.弱引用.虚引用. ①强引用的特点:垃圾回收机制绝不会回收它,即使内存不足时,JVM宁愿抛出OutOfMemor ...
- 了解java中垃圾回收机制
Java的垃圾回收机制是Java环境自带有的,它不像c语言的malloc申请空间后需要Free()函数来释放,而Java中的代码块中所申请的空间可在程序执行完成后自动释放,但是是有局限性的,代码块所占 ...
- python中垃圾回收机制
Python垃圾回收机制详解 一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题.在Python中,如果一个对象的引用数为0,Python虚拟 ...
- JVM中垃圾回收机制如何判断是否死亡?详解引用计数法和可达性分析 !
因为热爱,所以坚持. 文章下方有本文参考电子书和视频的下载地址哦~ 这节我们主要讲垃圾收集的一些基本概念,先了解垃圾收集是什么.然后触发条件是什么.最后虚拟机如何判断对象是否死亡. 一.前言 我们 ...
- java中垃圾回收机制中的引用计数法和可达性分析法(最详细)
首先,我这是抄写过来的,写得真的很好很好,是我看过关于GC方面讲解最清楚明白的一篇.原文地址是:https://www.zhihu.com/question/21539353
- PHP垃圾回收机制
一.引用计数基本知识 每个php变量存在一个叫"zval"的变量容器中,当一个变量被赋常量值时,就会生成一个zval变量容器.一个zval变量容器,除了包含变量的类型和值,还包括两 ...
- PHP-----浅谈垃圾回收机制
前言 大多数编程语言都会有自身的垃圾回收机制,php也不例外.经常听很多人说gc,也就是垃圾回收器,全程为Garbage Collection. 在php5.3之前,是不包括垃圾回收机制的,也没有专门 ...
- 深入了解C#系列:谈谈C#中垃圾回收与内存管理机制
今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...
- C#中垃圾回收与内存管理机制
今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...
随机推荐
- POJ2182 Lost Cows 题解
POJ2182 Lost Cows 题解 描述 有\(N\)(\(2 <= N <= 8,000\))头母牛,每头母牛有自己的独一无二编号(\(1..N\)). 现在\(N\)头母牛站成一 ...
- 模块 random 随机
random 随机数 0 导入 >>> import random 1 random 随机小数 random.random() # 大于0且小于1之间的小数 0.7664338663 ...
- 面向对象(OO)第一阶段学习总结
前言:对OO本阶段作业情况说明 本阶段一共完成三次作业,第一次主要是在主方法里面进行编程,也就是和之前C差不多,而随着学习的深入,慢慢了解到面向对象与面向过程的区别.作业的难度也在慢慢增大,后两次都用 ...
- Netty耗时的业务逻辑应该写在哪儿,有什么注意事项?
更多技术分享可关注我 前言 Netty以高性能著称,但是在实际使用中,不可避免会遇到耗时的业务逻辑,那么这些耗时操作应该写在哪儿呢,有什么注意的坑吗?本篇文章将一一总结. Netty线程调度模型回顾 ...
- C#是什么?
C# 是一个现代的.通用的.面向对象的编程语言,它是由微软(Microsoft)开发的,由 Ecma 和 ISO 核准认可的. C# 是由 Anders Hejlsberg 和他的团队在 .Net 框 ...
- HTML特殊转义字符——特殊符号
干货,见下图: 后期我会陆续更一些JavaScript的文章,大家可以一起学习交流.
- FZU - 2204 简单环形dp
FZU - 2204 简单环形dp 题目链接 n个有标号的球围成一个圈.每个球有两种颜色可以选择黑或白染色.问有多少种方案使得没有出现连续白球7个或连续黑球7个. 输入 第一行有多组数据.第一行T表示 ...
- block 的内存结构衍生出来的面试题
今天在群里看到大佬们在讨论一个面试题,问如下代码在 32bit 和 64bit 系统上分别报什么错误: #import <Foundation/Foundation.h> int main ...
- 新安装的eclipse配置好了环境变量后,打开还是出现A Java runtime environment错误
新安装的eclipse配置好了环境变量后,打开还是出现如下图的A Java runtime environment错误; 解决方法: 第一步: Windows环境下:把C:\Users\你的用户名 目 ...
- 第一章构建vue项目,代码仓库管理
一.安装node.js.vue-cli脚手架 1.安装node.js 下载地址:https://nodejs.org/en/download 查看版本号 node -v .npm -v 出现版本号即安 ...