リファレンスカウントの原理PHP 変数は「zval」と呼ばれるコンテナに保管されます。 zval コンテナには、変数の型と値の他に、情報の追加ビットを2つ含みます。 1つ目は「is_ref」と呼ばれ、変数が「参照集合」の一部かどうかを示すブール値 です。 このビットによって、通常の変数と参照を区別する方法を PHP エンジンが知ります。 &演算子によって作成されるように、PHP ではユーザーランドで参照を使えるので、 zval コンテナもメモリー使用状況を最適化するための内部的なリファレンスカウント機構を 持ちます。 追加情報の2つ目は「refcount」と呼ばれ、この1つの zval コンテナをどれだけ多くの 変数名(シンボルとも呼ばれます)が指すかを含みます。 シンボルは全てシンボルテーブルに保管され、スコープごとにシンボルテーブルの 1つがあります。 関数やメソッドごとのスコープばかりではなく、メインスクリプト用のスコープ (すなわち、ブラウザによってリクエストされたスクリプト)があります。 新しい変数が定数値を使って作成されるとき、zval コンテナが作成されます。 例えば、 例1 新規 zval コンテナを作成
この例では、新しいシンボル名(
例2 zval 情報を表示
上の例の出力は以下となります。 a: (refcount=1, is_ref=0)='new string' この変数を他の変数名に代入すると、refcount が増加します。
例3 zval の refcount を増加
上の例の出力は以下となります。 a: (refcount=2, is_ref=0)='new string'
この時、refcount は
例4 zval refcount を減少
上の例の出力は以下となります。 a: (refcount=3, is_ref=0)='new string' a: (refcount=2, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
次に、 複合型array や object のような複合型では、事情が少し 複雑になります。 scalar 値とは逆に、array と object では、それらのプロパティをそれら自身のシンボルテーブルに保管します。 これは、以下の例が3つの zval コンテナを作成することを意味します。
例5 array zval を作成
上の例の出力は、 たとえば以下のようになります。 a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 ) つまり、図で示すと 3つのzvalコンテナは a、meaning および number です。「refcount」の増減に同様のルールが適用されます。 下記では、配列に他の要素を追加して、既存の要素の内容をその値に設定します。
例6 既存の要素を配列に追加
上の例の出力は、 たとえば以下のようになります。 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 が 配列からの要素の除去は、スコープからシンボルを除去するようなものです。 そうすることによって、配列要素が指すコンテナの「refcount」は、減少します。 前と同じように、「refcount」がゼロに達すると、変数コンテナはメモリから 除去されます。前と同じように、これを示す例です。
例7 配列から要素を除去
上の例の出力は、 たとえば以下のようになります。 a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life' ) 次に、配列の要素として配列自体を追加すると、事情が面白くなります。 次の例で行います、そこでは、参照演算子にこっそり入りもします。 さもなければ PHP がコピーを作成するでしょう。
例8 それ自体の要素として配列自体を追加
上の例の出力は、 たとえば以下のようになります。 a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... ) つまり、図で示すと
配列変数 (a) が2番目の要素 (1) と同様に
「refcount」が ちょうど前のように、変数をアンセットするとシンボルが除去されます。 そして指す変数コンテナのリファレンスカウントがアンセットにより減少します。 従って、上記のコードを実行した後に変数 $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)=... ) つまり、図で示すと 片づけの問題この構造体を指すシンボルがいかなるスコープにももはや存在しないにもかかわらず、 配列要素「1」がこの同じ配列をまだ指すので、片づけられません。 それを指している外部シンボルがないので、ユーザーがこの構造体を片付ける方法が ありません。このようにしてメモリーリークとなります。 幸いにも、PHP はリクエスト終了後、このデータ構造を片付けます、しかし、それ以前に これはメモリ内の貴重な空間を占めています。 この状態は、「親」要素に再帰する「子」を持つ構文解析アルゴリズムなどの実装中に しばしば発生します。 もちろんオブジェクトでも同じ状態が起こり得ます。オブジェクトは常に暗黙のうちに 参照によって使われるので、実はその状態がより起こりそうです。 これが 1、2 回起こるだけであるならば、これは問題でないかもしれません。しかし、 これらのメモリ損失の数千または数百万さえあるならば、これは明らかに問題になり始めます。 これは、例えばリクエストが基本的に決して終わらないデーモンのような、長くかかる 実行中のスクリプトや、単体テストの大規模な集合で特に問題があります。 後者は、eZ コンポーネント・ライブラリのテンプレート・コンポーネントに対して 単体テストを実行中に問題を引き起こしました。 事例によっては、2GB 以上のメモリが必要でしたが、テストサーバーは全く 持っていませんでした。 |