各 PHP 変数は、「zval」と呼ばれる変数コンテナーに存在します。 zval 変数コンテナには、変数の型と値に加えて、2 バイトの追加情報が含まれています。 1 つ目は「is_ref」で、この変数が参照セットに属しているかどうかを識別するために使用されるブール値です。このバイトを通じて、PHP エンジンは通常の変数と参照変数を区別できます。PHP ではユーザーが & を使用してカスタム参照を使用できるため、zval 変数コンテナーにはメモリ使用量を最適化するための内部参照カウント メカニズムもあります。 2 番目の追加バイトは「refcount」で、この zval 変数コンテナーを指す変数 (シンボルとも呼ばれます) の数を示すために使用されます。すべてのシンボルはシンボル テーブルに存在し、各シンボルにはスコープ (スコープ) があり、メイン スクリプト (例: ブラウザを通じて要求されたスクリプト)、および各関数またはメソッドにもスコープがあります。
変数に定数値が割り当てられると、次の例のように zval 変数コンテナーが生成されます:
例 #1 新しい zval コンテナーを作成します
<?php $a = "new string"; ?>
上記の例では、新しい変数 a はスコープ内で生成される電流。そして、型が文字列、値が新しい文字列の変数コンテナが生成されます。追加の 2 バイトの情報では、カスタム参照が生成されないため、「is_ref」はデフォルトで FALSE に設定されます。この変数コンテナを使用する変数が 1 つだけであるため、「refcount」は 1 に設定されます。「refcount」が 1 の場合、Xdebug がインストールされている場合は、関数 xdebug_debug_zval() を渡すことができます。 「refcount」と「is_ref」の値を表示します。
例 #2 zval 情報の表示
<?php 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'
このとき、同じ変数コンテナが変数 a に関連付けられており、PHP は生成された変数コンテナを必要のないときにコピーしないため、参照の数は 2 になります。変数コンテナは、「refcount」が 0 になると破棄されます。変数コンテナに関連付けられた変数がそのスコープを離れるとき (例: 関数の実行が終了するとき)、または関数 unset() が変数に対して呼び出されるとき、「refcount」次の例が示すように、1 ずつ減らされます:
例 #4 zval の refcount の削減
<?php $a = "new string"; $c = $b = $a; xdebug_debug_zval( 'a' ); unset( $b, $c ); xdebug_debug_zval( 'a' ); ?>
上記のルーチンは出力します:
a: (refcount=3, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
ここで unset($a); を実行すると、型と値が含まれます。変数コンテナはメモリから削除されます。
複合型
配列やオブジェクトなどの複合型を考慮すると、状況は少し複雑になります。スカラー (スカラー) 型の値とは異なり、配列およびオブジェクト型の変数は、そのメンバーまたはプロパティを独自のシンボル テーブルに格納します。これは、次の例では 3 つの zval 変数コンテナーが生成されることを意味します。
例 #5 配列 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 )
例 #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' )
または、グラフィカル表示は次のとおりです。
上記の xdebug 出力情報から、元の配列要素と新しく追加された配列要素が、「refcount」 2 の同じ zval 変数コンテナーに関連付けられていることがわかります。 Xdebug に 2 つの値が表示される 「life」の zval 変数コンテナは実際には同じものです。 関数 xdebug_debug_zval() はこの情報を表示しませんが、メモリ ポインター情報を表示することで確認できます。
配列内の要素の削除は、スコープから変数を削除するのと似ています。同様に、「refcount」が 0 の場合、配列内の要素が配置されているコンテナーの「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)=... )
またはグラフィカルに
配列変数 (a) が配列の 2 番目の要素でもあることがわかります。配列 (1) 指す変数コンテナの「refcount」は 2 です。上記の出力の「...」は、再帰操作が発生したことを示します。この場合、明らかに、「...」が元の配列を指していることを意味します。
前と同じように、変数に対して unset を呼び出すとシンボルが削除され、それが指す変数コンテナ内の参照の数も 1 つ減ります。したがって、上記のコードを実行した後、変数 $a に対して unset を呼び出すと、変数 $a と配列要素 "1" が指す変数コンテナへの参照の数は、"2" から "1" に 1 減ります。次の例で説明します:
例 #9 は $a
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
を破棄するか、次のようにグラフィカルに表示します:
クリーンアップの問題 (クリーンアップの問題)
この構造体 (つまり、変数コンテナー) を指すスコープ内にシンボルはもうありませんが、配列要素 "1" は依然として配列自体を指しているため、このコンテナーはクリアできません。他にそれを指すシンボルがないため、ユーザーは構造体をクリアする方法がなく、メモリ リークが発生します。幸いなことに、PHP はリクエストの最後にこのデータ構造をクリアしますが、PHP がデータ構造をクリアする前に、メモリ内の大量のスペースを消費します。これは、解析アルゴリズムを実装している場合、または子要素がその親を指すようにするなどの他の作業を行っている場合によく発生します。もちろん、同じ状況がオブジェクトでも発生する可能性があります。実際、オブジェクトは常に暗黙的に参照されるため、オブジェクトで発生する可能性が高くなります。
上記の状況が 1 回か 2 回だけ発生する場合は問題ありませんが、メモリ リークが数千回、さらには数十万回発生した場合、これは明らかに大きな問題です。リクエストをめったに終了しないデーモンや単体テストの大規模なセットなど、長時間実行されるスクリプトでは、後者 (「単体テストで大規模なスイートを使用すると問題が発生する」を参照) は 2GB のメモリを必要とし、平均的なテスト サーバーはメモリを必要とします。そんなに大きなメモリ容量はありません。