前言

大多数编程语言都会有自身的垃圾回收机制,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. yum安装命令:遇到的问题报错如下: File "/usr/bin/yum", line 30 except KeyboardInterrupt, e: 通过看报错可以了解到是使用了python2的语法,所以了解到当前yum使用的Python2,因为我单独安装了python3,且python3设置为默认版本了,所以导致语法问题 解决方法: 使用python2.6 yum install

    1.安装zip yum install -y unzip zip 2.安装lrszs yum -y install lrzsz 3.安装scp 遇到下面的问题: 结果提示: No package sc ...

  2. 426. Convert Binary Search Tree to Sorted Doubly Linked List把bst变成双向链表

    [抄题]: Convert a BST to a sorted circular doubly-linked list in-place. Think of the left and right po ...

  3. SD

    Offer(Tcode:VA23;Table: vbak and vbap) billing(Tcode:VF03;Table:vbrk and vbrp) Offer(quotation)-> ...

  4. [leetcode]364. Nested List Weight Sum II嵌套列表加权和II

    Given a nested list of integers, return the sum of all integers in the list weighted by their depth. ...

  5. 将Promise融会贯通之路

    前端初学者经常会问,我如何在ajax1结束之后才启动ajax2呢?我怎么做才能在所有的ajax结束之后触发某程序呢?亦或是哎真是烦,5个ajax套在一起,原来的逻辑是什么呀! 一个稍微有点经验的前端程 ...

  6. github windows配置以及ssh生成 Permission denied (publickey)

    1:进入cmd命令下,或者可以使用GIt工具   (如果出现了 Permission denied 或者配置多个SSH Key跳第6步) git工具  下载地址:https://git-scm.com ...

  7. 通过类名或者jar名查询所在jar包

    一.问题 例如我想查看一下FilterSecurityInterceptor的源码,但是我不知道它在maven依赖中的哪个jar包中 二.解决方案 http://www.findmaven.net/ ...

  8. java多线程系列8 高级同步工具(2)CountDownLatch

    CountDownLatch,计数器的初始值为线程的数量.每当一个线程完成了自己的任务后, 计数器的值就会减1.当计数器值到达0时,它表示所有的线程已经完成了任务, 然后在闭锁上等待的线程就可以恢复执 ...

  9. 第二次spring会议

    今天所做之事: 我用C#用DelectText对行数进行了定义,刚开始写代码有点无从下手. 遇到的问题:刚开始用datagridView进行了文本的输入,但是它更适合EXCEL之类的数据计算不符合我们 ...

  10. tab下图片要求

    下面是每个tab的属性: 属性 类型 必填 说明 pagePath String 是 页面路径,必须在pages中先定义 text String 是 tab上按钮文字 iconPath String ...