本文首发于 PHP 垃圾回收与内存管理指引,转载请注明出处。
本文将要讲述 PHP 发展历程中的垃圾回收及内存管理相关内容,文末给出 PHP 发展在各个阶段有关内存管理及垃圾回收(内核)参考资料值得阅读。php
在 PHP 5.2 及之前的版本中,PHP 的垃圾回收采用的是 引用计数 算法。html
引用计数基础知识git
php 的变量存储在「zval」变量容器(数据结构)中,「zval」属性包含以下信息:github
当一个变量被赋值时,就会生成一个对应的「zavl」变量容器。算法
要查看变量的「zval」容器信息(即查看变量的 is_ref 和 refcount),可使用 XDebug 调试工具的 xdebug_debug_zval() 函数。数组
安装 XDebug 扩展插件的方法能够查看 这个教程,有关XDebug 使用方法请阅读 官方文档。缓存
假设,咱们已经成功安装好 XDebug 工具,如今就能够来对变量进行调试了。性能优化
若是咱们的 PHP 语句只是对变量进行简单赋值时,is_ref 标识值为 0,refcount 值为 1;若将这个变量做为值赋值给另外一个变量时,则增长 zval 变量容器的 refcount 计数;同理,销毁(unset)变量时,「refcount」相应的减去 1。php7
请看下面的示例:数据结构
<?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,在这个过程当中发生如下几个操做:
这里只是简单对「写时复制」进行介绍,感兴趣的朋友能够阅读文末给出的参考资料进行更加深刻的研究。
引用传值(&)的「引用计数」规则同普通赋值语句同样,只是 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
上面的引用计数示意图以下:
从图中咱们发现复合类型的引用计数规则基本上同标量的计数规则同样,就给出的示例来讲,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)
大体示意图以下:
。
虽然,复合类型的引用计数规则同标量类型大体相同,可是若是引用的值为变量自身(即循环应用),在处理不当时,就有可能会形成内存泄露的问题。
让咱们来看看下面这个对数组进行引用传值的示例:
<?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 数组,但内存并无被回收,整个处理过程的示意图以下:
能够看到对于这块内存,再也没有符合表(变量)指向了,因此 PHP 没法完成内存回收,官方给出的解释以下:
尽管再也不有某个做用域中的任何符号指向这个结构 (就是变量容器),因为数组元素 “1” 仍然指向数组自己,因此这个容器不能被清除 。由于没有另外的符号指向它,用户没有办法清除这个结构,结果就会致使内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,可是在 php 清除以前,将耗费很多内存。若是你要实现分析算法,或者要作其余像一个子元素指向它的父元素这样的事情,这种状况就会常常发生。固然,一样的状况也会发生在对象上,实际上对象更有可能出现这种状况,由于对象老是隐式的被引用。 - 摘自 官方文档 Cleanup Problems
简单来讲就是「引用计数」算法没法检测并释放循环引用所使用的内存,最终致使内存泄露。
因为引用计数算法存在没法回收循环应用致使的内存泄露问题,在 PHP 5.3 以后对内存回收的实现作了优化,经过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上作出了以下几个方面的加强:
下图(来自 PHP 手册),展现了新的回收算法执行过程:
整个过程为:
采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。
你能够从 PHP 手册 的回收周期 了解更多,也能够阅读文末给出的参考资料。
PHP 5 中 zval 实现上的主要问题:
PHP 7 中的 zval 数据结构实现的调整:
最基础的变化就是 zval 须要的内存 再也不是单独从堆上分配,再也不由 zval 存储引用计数。
复杂数据类型(好比字符串、数组和对象)的引用计数由其自身来存储。 - 摘自 Internal value representation in PHP 7 - Part 1【 译】
这种实现的优点:
更具体的有关 PHP 7 zval 实现和内存优化细节能够阅读 深刻理解 PHP7 内核之 zval 和 Internal value representation in PHP 7 - Part 1译。
Internal value representation in PHP 7 - Part 1【译】
Internal value representation in PHP 7 - Part 2【译】
浅谈 PHP5 中垃圾回收算法 (Garbage Collection) 的演化
Confusion about PHP 7 refcount
引用计数系统中的同步周期回收 (Concurrent Cycle Collection in Reference Counted Systems) 论文