加入收藏 | 设为首页 | 会员中心 | 我要投稿 我爱故事小小网_铜陵站长网 (http://www.0562zz.com/)- 视频终端、云渲染、应用安全、数据安全、安全管理!
当前位置: 首页 > 站长学院 > PHP教程 > 正文

深入介绍PHP垃圾回收及内存管理相关内容

发布时间:2022-07-11 12:54:40 所属栏目:PHP教程 来源:互联网
导读:PHP 垃圾回收与内存管理指引 本文将要讲述 PHP 发展历程中的垃圾回收及内存管理相关内容。 引用计数 在 PHP 5.2 及以前的版本中,PHP 的垃圾回收采用的是 引用计数 算法。 引用计数基础知识 引用计数基础知识 php 的变量存储在「zval」变量容器(数据结构)
  PHP 垃圾回收与内存管理指引
  本文将要讲述 PHP 发展历程中的垃圾回收及内存管理相关内容。
 
  引用计数
  在 PHP 5.2 及以前的版本中,PHP 的垃圾回收采用的是 引用计数 算法。
 
  引用计数基础知识
  引用计数基础知识
 
  php 的变量存储在「zval」变量容器(数据结构)中,「zval」属性包含如下信息:
 
  当前变量的数据类型;
  当前变量的值;
  用于标识变量是否为引用传递的 is_ref 布尔类型标识;
  指向该「zval」变量容器的变量个数的 refcount 标识符(即这个 zval 被引用的次数,注意这里的引用不是指引用传值,注意区分)。
  当一个变量被赋值时,就会生成一个对应的「zavl」变量容器。【推荐学习:PHP视频教程】
 
  查看变量 zval 容器信息
  要查看变量的「zval」容器信息(即查看变量的 is_ref 和 refcount),可以使用 XDebug 调试工具的 xdebug_debug_zval() 函数。
 
  安装 XDebug 扩展插件的方法可以查看 这个教程(https://github.com/huliuqing/phpnotes/issues/58),有关XDebug 使用方法请阅读 官方文档(https://xdebug.org/docs/)。
 
  假设,我们已经成功安装好 XDebug 工具,现在就可以来对变量进行调试了。
 
  查看普通变量的 zval 信息
  如果我们的 PHP 语句只是对变量进行简单赋值时,is_ref 标识值为 0,refcount 值为 1;若将这个变量作为值赋值给另一个变量时,则增加 zval 变量容器的 refcount 计数;同理,销毁(unset)变量时,「refcount」相应的减去 1。
 
  请看下面的示例:
 
 
 
 
 
 
 
 
 
 
 
 
 
  <?php
 
  // 变量赋值时,refcount 值等于 1
 
  $name = 'liugongzi';
 
  xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
 
   
 
  // $name 作为值赋值给另一个变量, refcount 值增加 1
 
  $copy = $name;
 
  xdebug_debug_zval('name'); // (refcount=2, is_ref=0)string 'liugongzi' (length=9)
 
   
 
  // 销毁变量,refcount 值减掉 1
 
  unset($copy);
 
  xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
 
  写时复制
  写时复制(Copy On Write:COW),简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时,才申请新空间来保存值内容以减少对内存的占用。 - TPIP 写时复制
  通过前面的简单变量的 zval 信息我们知道 $copy 和 $name 共用 zval 变量容器(内存),然后通过 refcount 来表示当前这个 zval 被多少个变量使用。
 
  看个实例:
 
 
 
 
 
 
 
 
 
 
 
 
  <?php
 
  $name = 'liugongzi';
 
  xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)
 
   
 
  $copy = $name;
 
  xdebug_debug_zval('name'); // name: (refcount=2, is_ref=0)string 'liugongzi' (length=9)
 
   
 
  // 将新的值赋值给变量 $copy
 
  $copy = 'liugongzi handsome';
 
  xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)
 
  xdebug_debug_zval('copy'); // copy: (refcount=1, is_ref=0)='liugongzi handsome'
 
  注意到没有,当将值 liugongzi handsome 赋值给变量 $copy 时,name 和 copy 的 refcount 值都变成了 1,在这个过程中发生以下几个操作:
 
  将 $copy 从 $name 的 zval(内从)中分离出来(即复制);
  将 $name 的 refcount 减去 1;
  对 $copy 的 zval 进行修改(重新赋值和修改 refcount);
  这里只是简单对「写时复制」进行介绍,感兴趣的朋友可以阅读文末给出的参考资料进行更加深入的研究。
 
  查看引用传递变量的 zval 信息
  引用传值(&)的「引用计数」规则同普通赋值语句一样,只是 is_ref 标识的值为 1 表示该变量是引用传值类型。
 
  我们现在来看看引用传值的示例:
 
 
 
 
 
 
 
 
 
 
  <?php
 
  $age = 'liugongzi';
 
  xdebug_debug_zval('age'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
 
   
 
  $copy = &$age;
 
  xdebug_debug_zval('age'); // (refcount=2, is_ref=1)string 'liugongzi' (length=9)
 
   
 
  unset($copy);
 
  xdebug_debug_zval('age'); // (refcount=1, is_ref=1)string 'liugongzi' (length=9)
 
  复合类型的引用计数
  与标量类型(整型、浮点型、布尔型等)不同,数组(array)和对象(object)这种符合类型的引用计数规则会稍复杂一些。
 
  为了更好的说明,还是先看看数组的引用计数示例:
 
 
 
 
 
 
 
 
 
  $a = array( 'meaning' => 'life', 'number' => 42 );
 
  xdebug_debug_zval( 'a' );
 
   
 
  // a:
 
  // (refcount=1, is_ref=0)
 
  // array (size=2)
 
  //  'meaning' => (refcount=1, is_ref=0)string 'life' (length=4)
 
  //  'number' => (refcount=1, is_ref=0)int 42
 
  上面的引用计数示意图如下:
 
  709d6945b0221f4711181ab8aa0f3f1.png
 
  从图中我们发现复合类型的引用计数规则基本上同标量的计数规则一样,就给出的示例来说,PHP 会创建 3 个 zval 变量容器,一个用于存储数组本身,另外两个用于存储数组中的元素。
 
  添加一个已经存在的元素到数组中时,它的引用计数器 refcount 会增加 1。
 
 
 
 
 
 
 
 
 
 
 
 
  $a = array( 'meaning' => 'life', 'number' => 42 );
 
  xdebug_debug_zval( 'a' );
 
  $a['life'] = $a['meaning'];
 
  xdebug_debug_zval( 'a' );
 
   
 
  // a:
 
  // (refcount=1, is_ref=0)
 
  // array (size=3)
 
  //  'meaning' => (refcount=2, is_ref=0)string 'life' (length=4)
 
  //  'number' => (refcount=0, is_ref=0)int 42
 
  //  'life' => (refcount=2, is_ref=0)string 'life' (length=4)
 
  大致示意图如下:
 
  d921e9f94b9a70bb7f166c1e0a92ea5.png
 
  内存泄露
  虽然,复合类型的引用计数规则同标量类型大致相同,但是如果引用的值为变量自身(即循环应用),在处理不当时,就有可能会造成内存泄露的问题。
 
  让我们来看看下面这个对数组进行引用传值的示例:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  <?php
 
  // @link http://php.net/manual/zh/function.memory-get-usage.php#96280
 
  function convert($size)
 
  {
 
      $unit=array('b','kb','mb','gb','tb','pb');
 
      return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
 
  }
 
   
 
  // 注意:有用的地方从这里开始
 
  $memory = memory_get_usage();
 
   
 
  $a = array( 'one' );
 
   
 
  // 引用自身(循环引用)
 
  $a[] =&$a;
 
   
 
  xdebug_debug_zval( 'a' );
 
   
 
  var_dump(convert(memory_get_usage() - $memory)); // 296 b
 
   
 
  unset($a); // 删除变量 $a,由于 $a 中的元素引用了自身(循环引用)最终导致 $a 所使用的内存无法被回收
 
   
 
  var_dump(convert(memory_get_usage() - $memory)); // 568 b
 
  从内存占用结果上看,虽然我们执行了 unset($a) 方法来销毁 $a 数组,但内存并没有被回收,整个处理过程的示意图如下:
 
  f03feea09a4b7621135380f88142302.png
 
  可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:
 
  尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。 - 摘自 官方文档 Cleanup Problems
  简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。
 
  引用计数系统的同步周期回收
  由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:
 
  引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。
  引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。
  回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。

(编辑:我爱故事小小网_铜陵站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读