前言

大多数编程语言都会有自身的垃圾回收机制,php也不例外。经常听很多人说gc,也就是垃圾回收器,全程为Garbage Collection。

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

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

引用计数的基本知识

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

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

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

refcount用来表示这个zval变量容器的变量的个数。所有符号存在一个符号表当中,每个符号都有作用域。

通俗的讲:

  • refcount就是多少个变量是一样的用了相同的值,那么refcount就是这个值
  • is_ref就是当有变量用了&的形式进行赋值,那么is_ref的值就会增加
<?php
$a = "new string";
?>

在上面的代码中,变量a是在当前作用于中生成的,并且生成了类型为String和值为"new string"的变量容器。这个时候is_ref被默认的设置成了false,因为现在没有任何自定义的引用生成。refcount被设置成了1。我们可以用php来看到这些计数的变化,首先需要用到xdebug,所以php没有装上xdebug扩展的需要先装一下。

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

增加zval的引用计数

<?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";
$c = $b = $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);,包含类型和值的这个变量容器就会从内存中删除。

复合类型

当变量的类型为array或object这样的复合类型时,array和object类型的变量把他们的成员或属性存在自己的符号表中。

<?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的refcount和is_ref。下面这张图是从官网上扒下来的:

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

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( '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'
)
?>

如下图解释:

从数组中删除一个元素:

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' ); 输出:
a: (refcount=1, is_ref=0)=array (
'life' => (refcount=1, is_ref=0)='life'
)
?>

删除数组中一个元素,就是类似从作用于中删除一个变量,删除后数组中这个元素所在容器的refcount的值减少,当refcount为0时,这个变量容器就从内存中被删除。

将数组作为一个元素添加给自身:

<?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同时也是这个数组第二个元素,指向的变量容器中refcount的值为2,上面输出的“...”说明发生了递归操作,意味着"..."指向原始数组。

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

垃圾回收周期

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

基本思想是如果一个引用计数增加那么将继续被使用,当然就不再是垃圾。如果引用计数减少到零,所在变量容器将被清除。那么也就是说只有在引用计数减少到非零值时,才会产生垃圾周期。在一个垃圾周期中通过检查引用计数是否减1,并且检查哪些变量容器的引用次数为零,来发现哪些是垃圾。

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

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

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

在php中垃圾回收机制默认是打开的,在你的php.ini中可以手动设置,通过zend.enable_gc这个属性进行开启或关闭垃圾回收机制。当开启了垃圾回收机制后,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区具有固定的大小,当然你可以通过修改php源码文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES来修改根缓存区的大小(注意修改后需要重新编译php)。当关闭垃圾回收机制后,这个循环查找算法将不会执行,然而可能根会一直存在于根缓冲区中,不管在配置中是否激活了垃圾回收机制。

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

php的内存管理机制

现在我们已经知道了zval是怎么回事了。那么现在我们需要知道php的内存管理机制是怎么一回事。

var_dump(memory_get_usage());
$test = "这是测试啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage()); 输出(php5.6):
/var/www/html/node_test/phptest/phptest.php:51:
int(361896)
/var/www/html/node_test/phptest/phptest.php:53:
int(361928)
/var/www/html/node_test/phptest/phptest.php:55:
int(361896)

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

var_dump(memory_get_usage());
$test = "这是测试啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage()); 输出(php7.1):
/var/www/html/node_test/phptest/phptest.php:51:
int(361896)
/var/www/html/node_test/phptest/phptest.php:53:
int(361928)
/var/www/html/node_test/phptest/phptest.php:55:
int(361928)

而我在用php7时发现了这个问题,这就要说道php5和php7的内存管理机制和垃圾回收机制的不同了,这里暂且不表。我们继续往下走。

当在执行

$test = "这是测试啊";

内存的分配做了两件事:

  1. 为变量名分配内存,并存入符号表
  2. 为变量值分配内存

我们再看代码:

var_dump(memory_get_usage());
for($i=0;$i<100;$i++)
{
$a = "test".$i;
$$a = "hello";
}
var_dump(memory_get_usage());
for($i=0;$i<100;$i++)
{
$a = "test".$i;
unset($$a);
}
var_dump(memory_get_usage()); 输出:
/var/www/html/node_test/phptest/phptest.php:57:
int(363520)
/var/www/html/node_test/phptest/phptest.php:63:
int(372384)
/var/www/html/node_test/phptest/phptest.php:69:
int(369216)

为什么内存没有全部收回来呢?

因为php的核心结构Hashtable,在定义的时候不可能一次性分配足够多的内存块,所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以当存入100个变量的时候符号表不够用了就进行一次扩容,当unset()时只是放了为变量值分配的内存,但是为变量名分配的内存还是在符号表中的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。

php并不是只要内存不够就去向OS申请内存,而是先申请一大块内存,然后将其中一部分分给申请者,这样再有逻辑需要申请内存的时候,就不需要再向OS申请内存了,避免了重复申请,只有当一大块内存不够用的时候再去申请。而当释放内存时,php并非把内存还给了OS,而是把内存轨道自己维护的空闲内存列表,以便重复利用。

新版本的php(5.3版本之后)是如何处理垃圾内存的?

刚刚上面我们已经讲了,针对在php中环形引用导致的垃圾,产生了新的同步算法(GC算法),对于官网上的理论,我进行了理解:

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

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

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

关于垃圾回收的小知识点

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

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

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

PHP-----浅谈垃圾回收机制的更多相关文章

  1. 浅谈Chrome V8引擎中的垃圾回收机制

    垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...

  2. 浅谈V8引擎中的垃圾回收机制

    最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...

  3. 浅谈 JavaScript 垃圾回收机制

    github 获取更多资源 https://github.com/ChenMingK/WebKnowledges-Notes 在线阅读:https://www.kancloud.cn/chenmk/w ...

  4. 浅谈python垃圾回收机制

    引入 ​ 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,那 ...

  5. 垃圾回收机制GC知识再总结兼谈如何用好GC(转)

    作者:Jeff Wong 出处:http://jeffwongishandsome.cnblogs.com/ 本文版权归作者和博客园共有,欢迎围观转载.转载时请您务必在文章明显位置给出原文链接,谢谢您 ...

  6. 垃圾回收机制GC知识再总结兼谈如何用好GC

    一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般 ...

  7. 谈一谈python的垃圾回收机制

    [python的垃圾回收机制是怎么实现的] 在C语言时代程序员要负责内存的申请和释放,虽然这样的程序可以对资源进行精细的控制.但是它也有它的问题.这就要求程序员 要写许多与业务逻辑无关的内容在代码里面 ...

  8. 垃圾回收机制GC知识再总结兼谈如何用好GC(其他信息: 内存不足)

    来源 图像操作,易内存泄露,边界像素 一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对 ...

  9. JS 从内存空间谈到垃圾回收机制

     壹 ❀ 引 从事计算机相关技术工作的同学,对于内存空间相关概念多少有所耳闻,毕竟像我这种非计算机科班出身的人,对于栈堆,垃圾回收都能简单说道几句:当我明白JS 基本类型与引用类型数据存储方式不同,才 ...

随机推荐

  1. mysql,utf8,utf8mb4

    参考文章 https://www.cnblogs.com/beyang/p/7580814.html https://blog.csdn.net/testcs_dn/article/details/7 ...

  2. Nginx – rewrite 配置 URL重写及301跳转原理图

    Nginx – rewrite 配置 URL重写 官网:http://nginx.org/en/docs/http/ngx_http_rewrite_module.html 语法:rewrite re ...

  3. Unity 2018 By Example 2nd Edition

    Unity is the most exciting and popular engine used for developing games. With its 2018 release, Unit ...

  4. 150. Evaluate Reverse Polish Notation逆波兰表达式

    [抄题]: Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid operators are ...

  5. C++字符串结束标识

    用一个字符数组可以存放一个字符串中的字符.如: char str[12]={'I',' ','a','m',' ','h','a','p','p','y'}; 用一维字符数组str来存放一个字符串″I ...

  6. Django的内置登录、退出、修改密码方法

    Django中内置的登录.退出.修改密码方法. 1.url.py中使用django.contrib.auth中的views函数,django.views.generic中的TemplateView函数 ...

  7. .net core 中使用ef 访问mysql

    1.参考文档说修改项目文件添加,就得这么做,不然会报错 <ItemGroup> <DotNetCliToolReference Include="Microsoft.Ent ...

  8. SPARK安装三:SPARK集群部署

    使用2.3.0版本,因为公司生产环境是这个版本 一.下载安装 cd /opt wget https://archive.apache.org/dist/spark/spark-2.3.0/spark- ...

  9. Chapter3_操作符_其他操作符

    对java中其他操作符及一些注意事项的总结 (1)按位操作符 按位操作符操作基本整数类型中的单个二进制位,有与(&),或(|),非(~).按位操作符还可以和等号(=)联合使用,如~=,& ...

  10. xbeePRO900HP的几个关键参数

    xbee PRO 900HP又叫xbee PRO S3B,在模块的正面有S3B的字样: 因为用到这个模块的,多用的是digimesh组网固件,所以以下参数修改只针对digimesh的修改:市面上的xb ...