PHP はマネージ言語です。PHP プログラミングでは、プログラマはメモリ リソースの割り当てと解放を手動で処理する必要がありません (C を使用して PHP または Zend 拡張機能を作成する場合を除く)。これは、PHP 自体がガベージ コレクション メカニズム (ガベージ コレクション) を実装することを意味します。 )。公式 PHP Web サイト (php.net) にアクセスすると、PHP5 の 2 つの Current Branch バージョン (PHP5.2 と PHP5.3) が別々に更新されていることがわかります。これは、多くのプロジェクトが依然として PHP 5.2 バージョンを使用しているためです。 、5.3 バージョンは 5.2 と完全な互換性がありません。 PHP5.3 では PHP5.2 をベースに多くの改良が加えられており、その中でもガベージコレクションのアルゴリズムは比較的大きな変更となっています。この記事では、PHP5.2 と PHP5.3 のガベージ コレクション メカニズムについてそれぞれ説明し、この進化と改善が PHP を作成するプログラマに与える影響と注意すべき問題について説明します。
PHP 変数と関連メモリ オブジェクトの内部表現
最終的には、ガベージ コレクションは変数とそれに関連付けられたメモリ オブジェクトの操作です。そのため、PHP のガベージ コレクション メカニズムについて説明する前に、PHP における変数とそのメモリ オブジェクトの内部表現 (C ソース コードでの表現) を簡単に紹介しましょう。
PHP 公式ドキュメントでは、PHP の変数をスカラー型と複合型の 2 つのカテゴリに分類しています。スカラー型にはブール型、整数型、浮動小数点型、文字列が含まれ、複合型には配列、オブジェクト、リソースが含まれます。NULL もあり、これはどの型にも分割されず、別個のカテゴリになります。
これらの型はすべて、PHP 内で zval と呼ばれる構造によって統一的に表現されます。PHP ソース コードでは、この構造の名前は「_zval_struct」です。 zval の具体的な定義は、PHP ソース コードの「Zend/zend.h」ファイルにあります。以下は、関連するコードの抜粋です。
typedef Union _zvalue_value {
long lval; /* 長い値 */
double dval; /* double 値 */
構造体 {
char *val;
int len;
} str;
HashTable *ht; /* ハッシュ テーブルの値 */
zend_object_value obj;
}zvalue_value;
struct _zval_struct {
/* 変数情報 */
zvalue_value 値;
/* 値 */
zend_uint refcount__gc;
zend_uchar タイプ; /* アクティブなタイプ */
zend_uchar is_ref__gc;
};
ここで共用体「_zvalue_value」を使用してすべての変数の値を表すのは、zval が一度に 1 種類の変数しか表すことができないためです。 _zvalue_value には 5 つのフィールドしかないことがわかりますが、PHP には NULL を含めて 8 つのデータ型が存在します。では、PHP は 5 つのフィールドを使用して内部で 8 つの型を表現しているのでしょうか。これは、フィールドの再利用によって実現されています。畑を減らす目的。たとえば、PHP 内では、ブール型、整数、リソース (リソースの識別子が保存されている場合) は lval フィールドを介して保存されます。str は文字列を保存します。 PHP では配列は実際にはハッシュ テーブルです)、obj にはオブジェクトの種類が格納されます。すべてのフィールドが 0 または NULL に設定されている場合、PHP では NULL を意味するため、8 種類の値を格納するために 5 つのフィールドが使用されます。
現在のzvalの値(値の型は_zvalue_value)がどの型を表すかは、「_zval_struct」の型によって決まります。 _zval_struct は、C 言語での zval の特定の実装です。各 zval は変数のメモリ オブジェクトを表します。 value と type に加えて、_zval_struct には refcount__gc と is_ref__gc という 2 つのフィールドがあることがわかります。それらのサフィックスから、これら 2 つはガベージ コレクションに関連していると結論付けることができます。そうです、PHP のガベージ コレクションはこれら 2 つのフィールドに完全に依存しています。このうち、refcount__gc は、現在この zval を参照している変数がいくつかあることを示し、is_ref__gc は、現在の zval が参照によって参照されているかどうかを示します。これは、PHP の zval の「Write-On-Copy」メカニズムに関連しています。このトピックはこの記事の焦点ではないため、ここでは詳しく説明しません。読者は refcount__gc フィールドの役割だけを覚えておく必要があります。
PHP5.2 のガベージ コレクション アルゴリズム—参照カウント
PHP5.2 で使用されるメモリ リサイクル アルゴリズムは、有名な参照カウントです。このアルゴリズムの中国語訳は「参照カウント」と呼ばれ、その考え方は非常に直感的で簡潔です。つまり、メモリ オブジェクトの作成時に各メモリ オブジェクトにカウンタを割り当てます。 、カウンタは 1 に初期化されます (つまり、この時点では常にこのオブジェクトを参照する変数が存在します)。新しい変数がこのメモリ オブジェクトを参照するたびに、カウンタは 1 ずつ増加します。ガベージ コレクションのメカニズムが動作すると、カウンタが 0 のすべてのメモリ オブジェクトが破棄され、それらによって占有されていたメモリがリサイクルされます。 PHP のメモリ オブジェクトは zval、カウンタは refcount__gc です。
たとえば、次の PHP コードは、PHP5.2 カウンターの動作原理を示しています (カウンター値は xdebug を通じて取得されます)。
$val1 = 100; //zval(val1).refcount_gc = 1;
$val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2 (コピー時書き込みなので、現在 val2 と val1 は一緒に zval を参照します)
$val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1 (val2 はここで新しい zval を作成します)
unset($val1); //zval(val1).refcount_gc = 0 ($val1 によって参照される zval は使用できなくなり、GC によってリサイクルされます)
?>
参照カウントはシンプルで直感的で実装が簡単ですが、メモリ リークを引き起こしやすいという致命的な欠陥があります。多くの友人は、循環参照がある場合、参照カウントによってメモリ リークが発生する可能性があることに気づいたかもしれません。たとえば、次のコード:
$a = array();
$a[] = & $a;
unset($a);
?>
このコードでは、まず配列 a を作成し、次に a の最初の要素を参照によってポイントします。このとき、a の zval の refcount は 2 になります。このとき、変数 a の refcount を破棄します。 a が元々指していた zval は 1 ですが、次の図に示すように循環自己参照を形成するため、操作できなくなります。
灰色の部分は、もう存在しないことを意味します。 a が指す zval の refcount は 1 (その HashTable の最初の要素によって参照される) であるため、この zval は GC によって破壊されず、メモリのこの部分がリークされます。
ここで特に重要なことは、PHP はシンボル テーブル (シンボル テーブル) を通じて変数シンボルを格納し、配列やオブジェクトなどの各複合型には独自のシンボル テーブルがあることです。上記のコードでは、a と a [0] は 2 つのシンボルですが、a はグローバル シンボル テーブルに格納され、a[0] は配列自体のシンボル テーブルに格納されます。ここで、a と a[0] は、同じ zval (もちろん、シンボル a は後で破棄されます)。読者の皆様には、シンボル (Symbol) と zval の関係を区別することに注意していただければ幸いです。
PHP が動的ページ スクリプトにのみ使用される場合、動的ページ スクリプトのライフ サイクルは非常に短く、PHP はスクリプトの実行時にすべてのリソースが確実に解放されるため、このリークはそれほど重要ではない可能性があります。ただし、PHP は、もはや動的ページ スクリプトとしてのみ使用されるだけではないところまで発展しています。自動テスト スクリプトやデーモン プロセスなど、ライフ サイクルの長いシナリオで PHP を使用すると、多くのサイクルの後にメモリ リークが蓄積される可能性があります。大変なことになりますよ。これはセンセーショナルではありません。私がかつてインターンをしていた会社では、データ ストレージ サーバーと対話するために PHP で書かれたデーモン プロセスを使用していました。
参照カウントのこの欠陥のため、PHP5.3 ではガベージ コレクション アルゴリズムが改善されました。
PHP5.3 のガベージ コレクション アルゴリズム—参照カウント システムでの同時サイクル コレクション
PHP5.3 のガベージ コレクション アルゴリズムは依然として参照カウントに基づいていますが、リサイクル基準として単純なカウントは使用されなくなり、代わりに同期リサイクル アルゴリズムが使用されます。このアルゴリズムは、論文「Concurrent Cycle Collection」で IBM エンジニアによって提案されました。で提案された参照カウントシステム。
このアルゴリズムは 29 ページの論文を見れば誰でもわかると思いますが、私はこのアルゴリズムについて詳しく説明するつもりはありません (またその能力はありません)。興味のある方は上記の論文を読んでください (強くお勧めします)。この論文は素晴らしいです)。
ここでは、このアルゴリズムの基本的な考え方を簡単に説明することしかできません。
まず、PHP は固定サイズの「ルート バッファ」を割り当てます。このバッファは、固定数の zval を格納するために使用されます。変更する必要がある場合は、ソース コード内の定数 GC_ROOT_BUFFER_MAX_ENTRIES を変更する必要があります。 Zend/zend_gc.c を選択し、コンパイルを再起動します。
上記のことから、zval に参照がある場合、それはグローバル シンボル テーブル内のシンボルによって参照されるか、複合型を表す他の zval 内のシンボルによって参照されることがわかります。したがって、zval にはいくつかのルートが考えられます。ここでは、PHP がこれらの考えられるルートを検出する方法については説明しません。これは非常に複雑な問題です。つまり、PHP には、これらの考えられるルート zval を検出し、ルート バッファーに入れる方法があります。
ルートバッファがいっぱいになると、PHP はガベージコレクションを実行します。リサイクルアルゴリズムは次のとおりです。
1. 各ルート バッファーのルート zval について、深さ優先トラバーサル アルゴリズムに従ってトラバースできるすべての zval をトラバースし、各 zval の refcount を 1 ずつデクリメントします。同時に、同じ zval のデクリメントを避けるために、 (異なる可能性があるため) ルートは同じ zval に複数回トラバースできます)、zval が 1 ずつデクリメントされるたびに、「デクリメント済み」としてマークされます。
2. 各バッファ内のルート zval を深さ優先で再度トラバースします。zval の refcount が 0 でない場合は 1 を追加し、それ以外の場合は 0 のままにします。
3. ルート バッファー内のすべてのルートをクリアし (これらの zval は破棄されるのではなく、バッファーからクリアされることに注意してください)、refcount が 0 のすべての zval を破棄し、メモリを再利用します。
完全に理解していなくても大丈夫です。PHP5.3 のガベージ コレクション アルゴリズムには次のような特徴があるということを覚えておいてください。
1. リサイクル サイクルは、refcount が減少するたびに開始されるわけではありません。ガベージ コレクションは、ルート バッファーがいっぱいになった後にのみ開始されます。
2. 循環参照問題を解決できる。
3. メモリ リークは常にしきい値未満に抑えることができます。
PHP5.2 と PHP5.3 のガベージ コレクション アルゴリズムのパフォーマンスの比較
現在の制限のため、実験を再設計することはしませんが、PHP マニュアルの実験を直接引用します。これら 2 つのパフォーマンスの比較については、PHP マニュアルの関連する章を参照してください: http://www.php。 net/manual/en/features.gc.performance-considerations.php。
1 つ目はメモリ リーク テストです。以下は PHP マニュアルの実験コードとテスト結果の図を直接引用しています。
クラス Foo
{
パブリック $var = '3.1415962654';
}
$baseMemory =memory_get_usage();
for ( $i = 0; $i {
$a = 新しい Foo;
$a->self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i )、memory_get_usage() - $baseMemory, "n";
}
}
?>
累積メモリ リークを引き起こす可能性のあるシナリオでは、PHP5.2 では継続的に累積メモリ リークが発生しますが、PHP5.3 では常にメモリ リークをしきい値 (ルート バッファ サイズに関連する) 以下に制御できることがわかります。
もう 1 つはパフォーマンスの比較です:
クラス Foo
{
パブリック $var = '3.1415962654';
}
for ( $i = 0; $i {
$a = 新しい Foo;
$a->self = $a;
}
echomemory_get_peak_usage(), "n";
?>
このスクリプトはループを 1,000,000 回実行するため、遅延時間は比較に十分なものになります。
次に、CLI を使用して、メモリ リサイクルをオンにし、メモリ リサイクルをオフにしてこのスクリプトを実行します。
時間 php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
#と
時間 php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
私のマシン環境では、実行時間はそれぞれ 6.4 秒、7.2 秒でした。PHP5.3 のガベージ コレクション メカニズムが遅くなることがわかりますが、影響は大きくありません。
ガベージコレクションアルゴリズムに関連する PHP 設定
php.ini の zend.enable_gc を変更することによって、PHP のガベージ コレクション メカニズムをオンまたはオフにすることができます。また、gc_enable() または gc_disable() を呼び出すことによって、PHP のガベージ コレクション メカニズムをオンまたはオフにすることができます。 PHP5.3 でガベージ コレクション メカニズムがオフになっている場合でも、PHP はルートの可能性をルート バッファーに記録しますが、ルート バッファーがいっぱいになると、PHP は自動的にガベージ コレクションを実行しません。もちろん、手動で gc_collect_cycles を呼び出すこともできます。 () 関数はメモリのリサイクルを強制します。