リファレンスカウントの原理

PHP 変数は「zval」と呼ばれるコンテナに保管されます。 zval コンテナには、変数の型と値の他に、情報の追加ビットを2つ含みます。 1つ目は「is_ref」と呼ばれ、変数が「参照集合」の一部かどうかを示すブール値 です。 このビットによって、通常の変数と参照を区別する方法を PHP エンジンが知ります。 &演算子によって作成されるように、PHP ではユーザーランドで参照を使えるので、 zval コンテナもメモリー使用状況を最適化するための内部的なリファレンスカウント機構を 持ちます。 追加情報の2つ目は「refcount」と呼ばれ、この1つの zval コンテナをどれだけ多くの 変数名(シンボルとも呼ばれます)が指すかを含みます。 シンボルは全てシンボルテーブルに保管され、スコープごとにシンボルテーブルの 1つがあります。 関数やメソッドごとのスコープばかりではなく、メインスクリプト用のスコープ (すなわち、ブラウザによってリクエストされたスクリプト)があります。

新しい変数が定数値を使って作成されるとき、zval コンテナが作成されます。 例えば、

例1 新規 zval コンテナを作成

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

この例では、新しいシンボル名(a)が現在のスコープで作成され、 新しい変数コンテナが string 型と値 new string で作成されます。 「is_ref」ビットはデフォルトで false にセットされます。なぜなら、ユーザーランド 参照が作成されたことがないからです。 この変数コンテナを利用するシンボルが1つだけあるので、「refcount」は 1 に設定されます。 「refcount」を持つ参照 ("is_ref" ビットが true の場合) が 1 の場合、 参照されていないかのように (つまり、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」がゼロに達すると、変数コンテナは破棄されます。 変数コンテナにリンクされたあらゆるシンボルがスコープを抜ける (たとえば関数が終わる) 場合、またはシンボルへの代入が解除された (たとえば unset が呼ばれた) 場合に「refcount」が減少します。 下記の例でこれを示します。

例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 では、それらのプロパティをそれら自身のシンボルテーブルに保管します。 これは、以下の例が3つの 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
)

つまり、図で示すと

単純配列に対する zval

3つのzvalコンテナは ameaning および number です。「refcount」の増減に同様のルールが適用されます。 下記では、配列に他の要素を追加して、既存の要素の内容をその値に設定します。

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

つまり、図で示すと

参照を使った単純配列に対する zval

上記の Xdebug 出力では、新旧両方の配列要素が今や、refcount が 2 である zval コンテナを指すことがわかります。 Xdebugの出力では、'life' という値の zval コンテナが2つ 表示されますが、それらは同一です。 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)=...
)

つまり、図で示すと

循環参照を使った配列に対する zval

配列変数 (a) が2番目の要素 (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)=...
)

つまり、図で示すと

メモリ・リークを実際に示す循環参照を使った配列を除去した後の zval

片づけの問題

この構造体を指すシンボルがいかなるスコープにももはや存在しないにもかかわらず、 配列要素「1」がこの同じ配列をまだ指すので、片づけられません。 それを指している外部シンボルがないので、ユーザーがこの構造体を片付ける方法が ありません。このようにしてメモリーリークとなります。 幸いにも、PHP はリクエスト終了後、このデータ構造を片付けます、しかし、それ以前に これはメモリ内の貴重な空間を占めています。 この状態は、「親」要素に再帰する「子」を持つ構文解析アルゴリズムなどの実装中に しばしば発生します。 もちろんオブジェクトでも同じ状態が起こり得ます。オブジェクトは常に暗黙のうちに 参照によって使われるので、実はその状態がより起こりそうです。

これが 1、2 回起こるだけであるならば、これは問題でないかもしれません。しかし、 これらのメモリ損失の数千または数百万さえあるならば、これは明らかに問題になり始めます。 これは、例えばリクエストが基本的に決して終わらないデーモンのような、長くかかる 実行中のスクリプトや、単体テストの大規模な集合で特に問題があります。 後者は、eZ コンポーネント・ライブラリのテンプレート・コンポーネントに対して 単体テストを実行中に問題を引き起こしました。 事例によっては、2GB 以上のメモリが必要でしたが、テストサーバーは全く 持っていませんでした。