引用计数基础

PHP 变量存储在一个名为“zval”的容器中。zval 容器除了包含变量的类型和值之外,还包含两个额外的信息位。第一个叫做“is_ref”,是一个布尔值,表示该变量是否属于一个“引用集”。通过这个位,PHP 引擎知道如何区分普通变量和引用。由于 PHP 允许用户级引用,如由 & 运算符创建,因此 zval 容器还有一个内部引用计数机制来优化内存使用。第二个额外的信息,叫做“refcount”,包含指向该 zval 容器的变量名(也称为符号)的数量。所有符号都存储在一个符号表中,每个作用域对应一个符号表。主脚本(即通过浏览器请求的脚本)有一个作用域,每个函数或方法也有一个作用域。

当使用常量值创建一个新变量时,就会创建一个 zval 容器,例如:

示例 #1 创建一个新的 zval 容器

<?php
$a
= "new string";
?>

在这种情况下,新的符号名 a 在当前作用域中创建,并创建一个新的变量容器,类型为 string,值为 new string。“is_ref” 位默认设置为 false,因为没有创建用户级引用。“refcount” 设置为 1,因为只有一个符号使用该变量容器。请注意,具有“refcount” 1 的引用(即“is_ref” 为 true)被视为非引用(即“is_ref” 为 false)。如果您安装了 » Xdebug,您可以通过调用 xdebug_debug_zval() 来显示此信息。

示例 #2 显示 zval 信息

<?php
$a
= "new string";
xdebug_debug_zval('a');
?>

上面的例子将输出

a: (refcount=1, is_ref=0)='new string'

将此变量分配给另一个变量名将增加 refcount。

示例 #3 增加 zval 的 refcount

<?php
$a
= "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>

上面的例子将输出

a: (refcount=2, is_ref=0)='new string'

这里的 refcount 为 2,因为同一个变量容器与 ab 都关联。PHP 足够聪明,在没有必要时不会复制实际的变量容器。当“refcount” 达到零时,变量容器就会被销毁。“refcount” 在任何与变量容器关联的符号离开作用域(例如,当函数结束时)或符号被取消分配(例如,通过调用 unset())时,都会减少一。以下示例展示了这一点

示例 #4 减少 zval refcount

<?php
$a
= "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset(
$c );
xdebug_debug_zval( 'a' );
?>

上面的例子将输出

a: (refcount=3, is_ref=0)='new string'
a: (refcount=2, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'

如果现在我们调用 unset($a);,变量容器,包括类型和值,将从内存中删除。

复合类型

arrayobject 这样的复合类型会稍微复杂一些。与 scalar 值不同,arrayobject 在它们自己的符号表中存储它们的属性。这意味着以下示例创建了三个 zval 容器

示例 #5 创建一个 array zval

<?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
)

或者以图形方式

Zvals for a simple array

三个 zval 容器是:ameaningnumber。类似的规则适用于增加和减少“refcounts”。下面,我们在数组中添加另一个元素,并将它的值设置为一个已经存在的元素的内容

示例 #6 将已经存在的元素添加到数组

<?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'
)

或者以图形方式

Zvals for a simple array with a reference

从上面的 Xdebug 输出中,我们看到旧的和新的数组元素现在都指向一个“refcount” 为 2 的 zval 容器。尽管 Xdebug 的输出显示了两个具有值 'life' 的 zval 容器,但它们实际上是同一个。xdebug_debug_zval() 函数没有显示这一点,但您可以通过同时显示内存指针来看到它。

从数组中删除一个元素就像从作用域中删除一个符号一样。这样做会减少数组元素指向的容器的“refcount”。同样,当“refcount” 达到零时,变量容器就会从内存中删除。同样,以下示例展示了这一点

示例 #7 从数组中删除一个元素

<?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'
)

现在,如果我们将数组本身作为数组的一个元素添加,事情就会变得有趣起来,我们在下一个示例中这样做,同时还偷偷地使用了一个引用运算符,否则 PHP 会创建一个副本

示例 #8 将数组本身作为它自身的元素添加

<?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)=...
)

或者以图形方式

Zvals for an array with a circular reference

您可以看到,数组变量 (a) 以及第二个元素 (1) 现在都指向一个“refcount”为 2 的变量容器。上面的显示中的“...”表示存在递归,当然,在这种情况下,“...”指向原始数组。

就像以前一样,取消设置变量会删除符号,并且它指向的变量容器的引用计数会减少一个。因此,如果我们在运行上面的代码后取消设置变量 $a,则 $a 和元素“1”指向的变量容器的引用计数将从“2”减少到“1”。这可以表示为

示例 #9 取消设置 $a

(refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

或者以图形方式

Zvals after removal of array with a circular reference demonstrating the memory leak

清理问题

虽然不再有任何范围中的符号指向此结构,但它无法被清理,因为数组元素“1”仍然指向同一个数组。由于没有外部符号指向它,因此用户无法清理此结构;因此会导致内存泄漏。幸运的是,PHP 会在请求结束时清理此数据结构,但在那之前,它会占用内存中的宝贵空间。如果您正在实现解析算法或其他具有子元素指向“父”元素的情况,则这种情况经常发生。同样,这种情况也可能发生在对象中,实际上更可能发生,因为对象总是隐式地按引用使用。

如果只发生一两次,这可能不是问题,但如果发生数千次甚至数百万次,这显然会成为问题。这在长时间运行的脚本中尤其成问题,例如守护进程,其中请求基本上永远不会结束,或者在大规模单元测试集中。后者在运行 eZ Components 库的模板组件的单元测试时会导致问题。在某些情况下,它需要超过 2 GB 的内存,而测试服务器没有足够的内存。

添加笔记

用户贡献的笔记 6 个笔记

16
匿名
9 年前
似乎无法检查特定类变量的引用计数,但您可以使用 xdebug_debug_zval('this'); 查看当前类实例中所有变量的引用计数。
12
匿名
9 年前
如果变量不在当前范围内,xdebug_debug_zval 将返回 null。
8
shkarbatov at gmail dot com
6 年前
“示例 #8 将数组本身作为其自身的元素添加”的结果将是 PHP7 的另一个

a: (refcount=2, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)

而不是
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)

PHP 7 中的内部值表示
https://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html
7
skymei at skymei dot cn
4 年前
$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

使用 PHP 7.3.12 (cli) 输出

a: (interned, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
4
yuri1308960477 at gmail dot com
5 年前
我的 php 版本:HP 7.1.25 (cli) (built: Dec 7 2018 08:20:45) (NTS)

$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

输出

a: (refcount=2, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1

如果 $a 是字符串值,则默认情况下“refcount”等于 2。
0
chxt2011 at 163 dot com
5 年前
我的 php 版本是 PHP 7.1.6 (cli),当我运行

$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

它显示
a: (refcount=0, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
To Top