この記事では、PHP の開発プロセスにおけるガベージ コレクションとメモリ管理に関連する内容について説明します。
参照カウント
PHP 5.2 以前のバージョンでは、PHP のガベージ コレクションは 参照カウント アルゴリズムを使用します。
参照カウントの基礎知識
phpの変数は「zval」変数コンテナ(データ構造)に格納されます。 ), "zval "この属性には次の情報が含まれます:
- 現在の変数のデータ型;
- 現在の変数の値;
- is_ref Boolean変数が参照によって渡されるかどうかを識別するために使用されるタイプ 識別子;
- は、「zval」変数コンテナー内の変数の数 (つまり、この zval が参照された回数) の refcount 識別子を指します。ここでの参照は値渡しを指すものではないことに注意してください。区別に注意してください)。
変数に値が割り当てられると、対応する「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 になります。 if この変数が別の変数に値として代入されると、zval 変数コンテナの refcount カウントが増加します。同様に、変数が破棄される (設定解除される) と、それに応じて「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
Copy On Write: COW、単に次のように説明されます: If に値を割り当てるとき変数の場合、新しい変数によって保存された値を格納するために新しいメモリは割り当てられません。代わりに、メモリは単にカウンタを通じて共有されます。新しいメモリは、変数を指す参照の 1 つの値が変更された場合にのみ割り当てられます。値の内容を保存してメモリ使用量を削減します。 - TPIP コピーオンライト
単純な変数の前述の zval 情報から、$copy と $name が zval 変数コンテナー ( Memory) を渡し、この zval で現在使用されている変数の数を示すために refcount を渡します。
例を見てみましょう:
<?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 ハンサム が変数 $copy に代入されると、refcount の値がこのプロセス中に次の操作が発生します:
- $name の zval (内部スレーブ) から $copy を分離します (つまり、コピー);
- $name の refcount から 1 を減算します;
- $copy の zval を変更します (refcount を再割り当てして変更します);
これは単なる「コピーオン」です-write" 操作 はじめに、興味のある友人は、記事の最後にある参考資料を読んでさらに詳しく調べることができます。
- 参照によって渡された変数の 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)
- 複合型の参照カウント
$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
$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 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:
- 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。
- 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。
- 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。
下图(来自 PHP 手册),展示了新的回收算法执行过程:
引用计数系统的同步周期回收过程
- 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);
- 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);
- 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);
- 删除所有 refcount 为 0 的可能根(步骤 D)。
整个过程为:
采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。
优化后的引用计数算法优势
- 将内存泄露控制在阀值内,这个由缓存区实现,达到缓冲区大小执行新一轮垃圾回收;
- 提升了垃圾回收性能,不是每次 refcount 减 1 都执行回收处理,而是等到根缓冲区满时才开始执行垃圾回收。
你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。
PHP 7 的内存管理
PHP 5 中 zval 实现上的主要问题:
- zval 常にヒープとは別にメモリを割り当てます zval
- 整数 (bool/null) であっても、常に参照カウントとリサイクル 情報を保存しますこの種のデータにはそのような情報は必要ない場合があります; オブジェクトまたはリソースを使用する場合、直接参照すると二重カウントが発生します;
- 一部の間接アクセスには、より適切な処理方法が必要です。たとえば、変数に格納されたオブジェクトへのアクセスには、4 つのポインターが間接的に使用されるようになりました (ポインター チェーンの長さは 4 です);
- 直接カウントとは、値が zval 間でのみ共有できることを意味します。これは、zval とハッシュテーブル キーの間で文字列を共有したい場合には機能しません (ハッシュテーブル キーも zval でない限り)。
に必要なメモリがヒープとは別に割り当てられなくなったことです. 参照カウントは zval によって保存されなくなりました。 複雑なデータ型 (文字列、配列、オブジェクトなど) の参照カウントは、それ自体で保存されます。 - PHP 7 の内部値表現より抜粋 - パート 1【翻訳】この実装の利点:
- 単純なデータ型は、別途メモリを割り当てる必要がなく、メモリを割り当てる必要もありません。カウントされる;
- これ以上、二重にカウントされることはありません。オブジェクトでは、オブジェクト自体に保存されているカウントのみが有効です;
- カウントは値自体によって保存されるようになったので (PHP には zval 変数コンテナー ストレージがあります)、非 zval のデータと共有できます。 zval やハッシュテーブル キーなどの構造体;
- 間接アクセスに必要なポインタの数が減ります。
PHP 7 zval の実装とメモリの最適化の詳細については、「PHP 7 カーネルにおける zval と内部値表現の詳細な理解 - パート 1」の翻訳を参照してください。 (https://www.npopov.com/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html)