1
0
mirror of https://github.com/php/doc-zh.git synced 2026-03-23 22:52:08 +01:00
Files
archived-doc-zh/features/gc.xml
Louis-Arnaud 6e065355ce Check translation for features (#1019)
* Fix features/: translations, security, missing content

- remote-files.xml: remove duplicated 如果
- sessions.xml: 新引力→吸引力
- persistent-connections.xml: fix 自以为→用作 translation error
- gc.xml: remove translator comment left in published text
- http-auth.xml: add htmlspecialchars() for XSS protection, sync with EN
- file-upload.xml: translate 2 untranslated English paragraphs,
  add missing "Uploading an entire directory" section

* Update file-upload.xml

根据 https://github.com/php/doc-zh?tab=readme-ov-file#有关在翻译后的中文文件中的空格与换行的说明 更新内容换行

---------

Co-authored-by: mowangjuanzi <mowangjuanzi@petalmail.com>
2026-03-17 18:28:01 +08:00

520 lines
23 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="utf-8"?>
<!-- $Revision$ -->
<!-- EN-Revision: 23e84882d6654f995166d17e5610af6bf00ef18c Maintainer: mowangjuanzi Status: ready -->
<!-- CREDITS: Luffy -->
<chapter xml:id="features.gc" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>垃圾回收</title>
<para>
本节介绍了新垃圾回收(也称为 GC机制作为 PHP 5.3 一部分的优点。
</para>
<sect1 xml:id="features.gc.refcounting-basics">
<title>引用计数基础</title>
<para>
PHP 变量存储在称为“zval”的容器中。zval 容器除了变量的类型和值之外还包含两个额外的信息位。第一个是“is_ref”是布尔值表示变量是否是“引用集合”的一部分。通过这个位PHP
引擎知道如何区分普通变量和引用。由于 PHP 允许用户自定义引用,通过 &amp; 运算符创建引用zval 容器还有内部引用计数机制来优化内存使用。第二个是“refcount”表示有多少个变量名也称为符号指向这个
zval 容器。所有符号都存储在一个符号表中,每个作用域都有一个符号表。主脚本(即通过浏览器请求的脚本)有一个作用域,每个函数或方法也有一个作用域。
</para>
<para>
当使用常量值创建新变量时,也会创建 zval 容器,例如
<example>
<title>创建新 zval 容器</title>
<programlisting role="php">
<![CDATA[
<?php
$a = "new string";
?>
]]>
</programlisting>
</example>
</para>
<para>
在这种情况下,新的符号名称 <literal>a</literal> 会在当前作用域中创建,并且会创建新的变量容器,其类型为 <type>string</type>,值为
<literal>new string</literal>。由于没有创建用户定义的引用“is_ref”位默认设置为 &false;。“refcount”设置为
<literal>1</literal>因为只有一个符号使用了这个变量容器。请注意具有“refcount”为 <literal>1</literal> 的引用(即"is_ref"为
&true;会视为非引用即“is_ref”为 &false;)。如果安装了 <link
xlink:href="&url.xdebug;">Xdebug</link>,可以通过调用 <function>xdebug_debug_zval</function> 来显示此信息。
</para>
<para>
<example>
<title>显示 zval 信息</title>
<programlisting role="php">
<![CDATA[
<?php
$a = "new string";
xdebug_debug_zval('a');
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
a: (refcount=1, is_ref=0)='new string'
]]>
</screen>
</example>
</para>
<para>
将这个变量赋值给另一变量名将增加 refcount 的计数。
</para>
<para>
<example>
<title>增加 zval 的 refcount</title>
<programlisting role="php">
<![CDATA[
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
a: (refcount=2, is_ref=0)='new string'
]]>
</screen>
</example>
</para>
<para>
这里的 refcount 是 <literal>2</literal>,因为同一个变量容器链接到 <varname>a</varname><varname>b</varname>。PHP
很聪明当没有必要的时候不会复制实际的变量容器。当“refcount”到 0 时,就会销毁变量容器。当链接到变量容器的任何符号离开作用域(例如函数结束时)或取消符号赋值(例如通过调用
<function>unset</function>“refcount”会减少 1。以下是示例
</para>
<para>
<example>
<title>减少 zval refcount</title>
<programlisting role="php">
<![CDATA[
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
a: (refcount=3, is_ref=0)='new string'
a: (refcount=2, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
]]>
</screen>
</example>
</para>
<para>
如果现在调用 <literal>unset($a);</literal>,变量容器,包含类型和值,会从内存中移除。
</para>
<sect2 xml:id="features.gc.compound-types">
<title>复合类型</title>
<para>
对于 <type>array</type><type>object</type> 这样的复合类型,情况会稍微复杂一些。与 <type>scalar</type>
值不同,<type>array</type><type>object</type> 的属性存储在自己的符号表中。这意味着以下示例将创建三个 zval 容器:
</para>
<para>
<example>
<title>创建 <type>array</type> zval</title>
<programlisting role="php">
<![CDATA[
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
a: (refcount=1, is_ref=0)=array (
'meaning' => (refcount=1, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42
)
]]>
</screen>
<para>图示:</para>
<mediaobject>
<alt>简单数组的 zval</alt>
<imageobject>
<imagedata fileref="en/features/figures/simple-array.png" format="PNG"/>
</imageobject>
</mediaobject>
</example>
</para>
<para>
这三个 zval 变量容器是 <varname>a</varname><varname>meaning</varname>
<varname>number</varname>。增加和减少“refcounts”的规则也适用于此。下面再向数组添加一个元素并将其值设置为已存在元素的内容
</para>
<para>
<example>
<title>添加已存在的元素到数组</title>
<programlisting role="php">
<![CDATA[
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
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'
)
]]>
</screen>
<para>图示:</para>
<mediaobject>
<alt>带有引用的简单数组的 zvals</alt>
<imageobject>
<imagedata fileref="en/features/figures/simple-array2.png" format="PNG"/>
</imageobject>
</mediaobject>
</example>
</para>
<para>
从上面的 Xdebug 输出中可以看到新旧的数组元素现在都指向“refcount”为 <literal>2</literal> 的 zval 容器。尽管 Xdebug
的输出显示了两个值为 <literal>'life'</literal> 的 zval 容器,但它们实际上是同一个。<function>xdebug_debug_zval</function>
函数没有显示这一点,但可以通过显示内存指针来看到它。
</para>
<para>
从数组中删除元素就像从作用域中删除符号一样。删除后数组元素指向的容器的“refcount”会减少。同样当“refcount”到 0 时,变量容器就会从内存中删除。再举个例子来说明这一点:
</para>
<para>
<example>
<title>从数组中删除元素</title>
<programlisting role="php">
<![CDATA[
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
a: (refcount=1, is_ref=0)=array (
'life' => (refcount=1, is_ref=0)='life'
)
]]>
</screen>
</example>
</para>
<para>
现在,如果将数组本身作为数组的一个元素添加进去,情况就会变得有趣起来。在下一个例子中这样做,并且偷偷加入引用运算符,否则 PHP 会创建副本:
</para>
<para>
<example>
<title>将数组添加为自身的一个元素</title>
<programlisting role="php">
<![CDATA[
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
]]>
</screen>
<para>图示:</para>
<mediaobject>
<alt>具有循环引用的数组 zval</alt>
<imageobject>
<imagedata fileref="en/features/figures/loop-array.png" format="PNG"/>
</imageobject>
</mediaobject>
</example>
</para>
<para>
可以看到数组变量(<varname>a</varname>)以及第二个元素(<varname>1</varname>现在都指向“refcount”为 <literal>2</literal>
的变量容器。上面显示的“...”表示存在递归,这在这种情况下意味着“...”指向原数组。
</para>
<para>
就像之前一样,清除变量会删除符号,并且指向的变量容器的引用计数会减少 1。因此如果在运行上述代码后清除变量 <varname>$a</varname>,那么
<varname>$a</varname> 和元素“1”所指向的变量容器的引用计数会减少 1从“2”变为“1”。可以表示为
</para>
<para>
<example>
<title>清除 <varname>$a</varname></title>
<screen>
<![CDATA[
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)
]]>
</screen>
<para>图示:</para>
<mediaobject>
<alt>在演示完内存泄漏的循环引用数组移除后的 zval</alt>
<imageobject>
<imagedata fileref="en/features/figures/leak-array.png" format="PNG"/>
</imageobject>
</mediaobject>
</example>
</para>
</sect2>
<sect2 xml:id="features.gc.cleanup-problems">
<title>清理问题</title>
<para>
虽然在任何作用域中都没有指向这个结构的符号却无法清理它因为数组元素“1”仍然指向同一个数组。由于没有外部符号指向它用户无法清理该结构因此会出现内存泄漏。幸运的是PHP
会在请求结束时清理这个数据结构,但在此之前,它会占用宝贵的内存空间。如果你正在实现解析算法或其他需要子级元素指向"父级"元素的情况会经常发生。当然object
也可能出现相同的情况,因为 object 始终隐式“<link linkend="language.oop5.references">引用</link>”。
</para>
<para>
如果这种情况只发生一两次,可能不是问题,但如果出现数千次,甚至数百万次的内存损失,显然就成了问题。这在长时间运行的脚本中尤为棘手,比如守护进程,其中请求基本上永远不会结束,或者在大量的单元测试集中。后者在运行
eZ Components 库的模板组件的单元测试时出现了问题。在某些情况下,它需要超过 2GB 的内存,而测试服务器并没有那么多内存可用。
</para>
</sect2>
</sect1>
<sect1 xml:id="features.gc.collecting-cycles">
<title>回收循环</title>
<para>
传统上,像 PHP 之前使用的引用计数内存机制无法解决循环引用内存泄漏的问题;然而,从 5.3.0 版本开始PHP 实施了<link
xlink:href="&url.gc-paper;">引用计数系统中的同步循环回收</link>论文中的同步算法来解决这个问题。
</para>
<para>
对算法的完全说明有点超出这部分内容的范围,将只介绍其中基础部分。首先,需要确立一些基本规则。如果 refcount 增加,则该变量仍在使用中,因此不是垃圾。如果 refcount
减少到 0则 zval 可以释放。这意味着只有当引用计数参数减少到非零值时,才能创建垃圾循环。其次,在垃圾循环中,可以通过检查是否可以将 refcount 减少 1并检查哪些
zval 的 refcount 为 0 来确定哪些部分是垃圾。
</para>
<para>
<mediaobject>
<alt>垃圾回收算法</alt>
<imageobject>
<imagedata fileref="en/features/figures/gc-algorithm.png" format="PNG"/>
</imageobject>
</mediaobject>
</para>
<para>
为避免不得不检查所有引用计数可能减少的垃圾循环,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。
</para>
<para>
在步骤 B 中,模拟删除每个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减"1"如果某个普通变量引用计数变成0了就对这个普通变量再做一次模拟删除。每个变量只能被模拟删除一次模拟删除后标记为灰确保不会对同一个变量容器减两次"1"。
</para>
<para>
在步骤 C 中模拟恢复每个紫色变量。恢复是有条件的当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次恢复后标记为黑基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。
</para>
<para>
算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历即可(最典型的深搜遍历)。复杂度为执行模拟操作的节点数正相关,不只是紫色的那些疑似垃圾变量。
</para>
<para>
对算法的工作原理有了基本的了解后,现在可以回顾一下如何与 PHP 集成。默认情况下PHP 的垃圾回收器是打开的。然而,有个 &php.ini;
设置可以进行更改:<link linkend="ini.zend.enable-gc">zend.enable_gc</link>
</para>
<para>
当打开垃圾回收器时,如上所述的循环查找算法将在根缓冲区满时执行。根缓冲区的大小是固定的,可以容纳 10,000 个可能的根(尽管可以通过更改
PHP 源代码中的 <literal>Zend/zend_gc.c</literal> 中的 <constant>GC_THRESHOLD_DEFAULT</constant> 常量并重新编译 PHP
来修改这个值)。当关闭垃圾回收器时,循环查找算法将永不运行。然而,无论是否使用此配置激活垃圾回收机制,可能根都将始终记录在根缓冲区中。
</para>
<para>
如果在垃圾回收机制关闭时,根缓冲区存满了可能的根,那么将不会记录进一步的可能根。算法永远不会分析那些没有记录的可能根。如果他们是循环引用的一部分,将永不会清除从而导致内存泄漏的产生。
</para>
<para>
即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。
</para>
<para>
除了改变配置中的 <link linkend="ini.zend.enable-gc">zend.enable_gc</link> 之外,还可以通过调用 <function>gc_enable</function>
<function>gc_disable</function> 来启用/禁用垃圾回收机制。调用这些函数与通过配置打开或关闭机制的效果相同。即使可能的根缓冲区尚未满,还可以强制回收循环。为此,可以使用
<function>gc_collect_cycles</function> 函数。该函数将返回算法回收的循环数量。
</para>
<para>
允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。因此,就在你调用<function>gc_disable</function>函数释放内存之前,先调用<function>gc_collect_cycles</function>函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。
</para>
</sect1>
<sect1 xml:id="features.gc.performance-considerations">
<title>性能方面考虑的因素</title>
<para>
在上一节我们已经简单的提到回收可能根有细微的性能上影响但这是把PHP 5.2与PHP 5.3比较时才有的。尽管在PHP 5.2中记录可能根相对于完全不记录可能根要慢些而PHP 5.3中对 PHP run-time 的其他修改减少了这个性能损失。
</para>
<para>
这里主要有两个领域对性能有影响。第一个是内存占用空间的节省,另一个是垃圾回收机制执行内存清理时的执行时间增加(run-time delay)。我们将研究这两个领域。
</para>
<sect2 xml:id="features.gc.performance-considerations.reduced-mem">
<title>内存占用空间的节省</title>
<para>
首先实现垃圾回收机制的整个原因是为了一旦先决条件满足通过清理循环引用的变量来节省内存占用。在PHP执行中一旦根缓冲区满了或者调用<function>gc_collect_cycles</function> 函数时就会执行垃圾回收。在下图中显示了下面脚本分别在PHP 5.2 和 PHP 5.3环境下的内存占用情况其中排除了脚本启动时PHP本身占用的基本内存。
</para>
<para>
<example>
<title>Memory usage example</title>
<programlisting role="php">
<![CDATA[
<?php
class Foo
{
public $var = '3.14159265359';
public $self;
}
$baseMemory = memory_get_usage();
for ( $i = 0; $i <= 100000; $i++ )
{
$a = new Foo;
$a->self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
?>
]]>
</programlisting>
<mediaobject>
<alt>Comparison of memory usage between PHP 5.2 and PHP 5.3</alt>
<imageobject>
<imagedata fileref="en/features/figures/gc-benchmark.png" format="PNG"/>
</imageobject>
</mediaobject>
</example>
</para>
<para>
在这个很理论性的例子中,我们创建了一个对象,这个对象中的一个属性被设置为指回对象本身。在循环的下一个重复(iteration)中,当脚本中的变量被重新复制时,就会发生典型性的内存泄漏。在这个例子中,两个变量容器是泄漏的(对象容器和属性容器)但是仅仅能找到一个可能根就是被unset的那个变量。在10,000次重复后(也就产生总共10,000个可能根)当根缓冲区满时就执行垃圾回收机制并且释放那些关联的可能根的内存。这从PHP 5.3的锯齿型内存占用图中很容易就能看到。每次执行完10,000次重复后执行垃圾回收并释放相关的重复使用的引用变量。在这个例子中由于泄漏的数据结构非常简单所以垃圾回收机制本身不必做太多工作。从这个图表中你能看到 PHP 5.3的最大内存占用大概是9 Mb而PHP 5.2的内存占用一直增加。
</para>
</sect2>
<sect2 xml:id="features.gc.performance-considerations.slowdowns">
<title>执行时间增加(Run-Time Slowdowns)</title>
<para>
垃圾回收影响性能的第二个领域是它释放已泄漏的内存耗费的时间。为了看到这个耗时时多少,我们稍微改变了上面的脚本,有更多次数的重复并且删除了循环中的内存占用计算,第二个脚本代码如下:
</para>
<para>
<example>
<title>GC性能影响</title>
<programlisting role="php">
<![CDATA[
<?php
class Foo
{
public $var = '3.14159265359';
public $self;
}
for ( $i = 0; $i <= 1000000; $i++ )
{
$a = new Foo;
$a->self = $a;
}
echo memory_get_peak_usage(), "\n";
?>
]]>
</programlisting>
</example>
</para>
<para>
运行这个脚本两次,一次将 <link linkend="ini.zend.enable-gc">zend.enable_gc</link> 设置打开,一次关闭。
</para>
<para>
<example>
<title>执行以上脚本</title>
<programlisting role="shell">
<![CDATA[
time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
]]>
</programlisting>
</example>
</para>
<para>
在我的机器上第一个命令持续执行时间大概为10.7秒而第二个命令耗费11.4秒。时间上增加了7%。然而执行这个脚本时内存占用的峰值降低了98%从931Mb 降到 10Mb。这个基准不是很科学或者并不能代表真实应用程序的数据但是它的确显示了垃圾回收机制在内存占用方面的好处。好消息就是对这个脚本而言在执行中出现更多的循环引用变量时内存节省的更多的情况下每次时间增加的百分比都是7%。
</para>
</sect2>
<sect2 xml:id="features.gc.performance-considerations.internal-stats">
<title>PHP内部 GC 统计信息</title>
<para>
在PHP内部可以显示更多的关于垃圾回收机制如何运行的信息。但是要显示这些信息你需要先重新编译PHP使benchmark和data-collecting code可用。你需要在按照你的意愿运行<literal>./configure</literal>前,把环境变量<literal>CFLAGS</literal>设置成<literal>-DGC_BENCH=1</literal>。下面的命令串就是做这个事:
</para>
<para>
<example>
<title>重新编译PHP以启用GC benchmarking</title>
<programlisting role="shell">
<![CDATA[
export CFLAGS=-DGC_BENCH=1
./config.nice
make clean
make
]]>
</programlisting>
</example>
</para>
<para>
当你用新编译的PHP二进制文件来重新执行上面的例子代码在PHP执行结束后你将看到下面的信息
</para>
<para>
<example>
<title>GC 统计数据</title>
<programlisting role="shell">
<![CDATA[
GC Statistics
-------------
Runs: 110
Collected: 2072204
Root buffer length: 0
Root buffer peak: 10000
Possible Remove from Marked
Root Buffered buffer grey
-------- -------- ----------- ------
ZVAL 7175487 1491291 1241690 3611871
ZOBJ 28506264 1527980 677581 1025731
]]>
</programlisting>
</example>
</para>
<para>
主要的信息统计在第一个块。你能看到垃圾回收机制运行了110次而且在这110次运行中总共有超过两百万的内存分配被释放。只要垃圾回收机制运行了至少一次根缓冲区峰值(Root buffer peak)总是10000.
</para>
</sect2>
<sect2 xml:id="features.gc.performance-considerations.conclusion">
<title>结论</title>
<para>
通常PHP中的垃圾回收机制仅仅在循环回收算法确实运行时会有时间消耗上的增加。但是在平常的(更小的)脚本中应根本就没有性能影响。
</para>
<para>
然而,在平常脚本中有循环回收机制运行的情况下,内存的节省将允许更多这种脚本同时运行在你的服务器上。因为总共使用的内存没达到上限。
</para>
<para>
这种好处在长时间运行脚本中尤其明显诸如长时间的测试套件或者daemon脚本此类。同时对通常比Web脚本运行时间长的<link xlink:href="&url.php.gtk;">PHP-GTK</link>应用程序,新的垃圾回收机制,应该会大大改变一直以来认为内存泄漏问题难以解决的看法。
</para>
</sect2>
</sect1>
</chapter>
<!-- Keep this comment at the end of the file
vim600: syn=xml fen fdm=syntax fdl=2 si
vim: et tw=78 syn=sgml
vi: ts=1 sw=1
-->