PHPのGCは循環参照を回収できる
PHPで親子関係のオブジェクトが相互に参照を持つ ($parent->children
がありかつ $child->parent
がある) ケースの話をしていたとき、循環参照の話題が出たのでふと気になって調査してみました。
結論からいうと、PHPは5.2まで、単純な参照カウンタ方式のGCのみを採用していました。5.3からは、参照カウンタ方式に加えて、循環参照を回収するGCも併用するようになりました。
PHPの変数は、基本的には参照カウンタが0になった時点でメモリを解放します。が、それだけでは、循環参照があると0までカウンタが落ち切らない変数が発生します。かといって、毎回循環参照をチェックするとパフォーマンス低下が発生します。そこで、GC監視下の変数が一定数 (コンパイル時のGC_ROOT_BUFFER_MAX_ENTRIES定数、通常は1万) を超えたときに初めて、循環参照を片付け始めるという戦略を取ります。
この循環参照用のガベージコレクタを使うかどうかは、 php.ini または gc_enable() / gc_disable() で切り替えられます。普通は有効にすべきですが、試しに無効にしてみて、循環参照がどうなるかを確認してみました。
<?php | |
/* | |
* w/o circular reference collector: | |
* php -d zend.enable_gc=0 circular-ref.php | |
* | |
* with circular reference collector: | |
* php -d zend.enable_gc=1 circular-ref.php | |
*/ | |
if (version_compare(PHP_VERSION, '5.3.0') < 0) { | |
exit(1); | |
} | |
function createCircularRefPairs($count) | |
{ | |
$pairs = []; | |
for ($i = 0; $i < $count; $i++) { | |
$a = new stdClass(); | |
$b = new stdClass(); | |
$a->peer = $b; | |
$b->peer = $a; | |
$pairs[] = $a; | |
} | |
return $pairs; | |
} | |
for ($i = 0; $i < 1000; $i++) { | |
$pairs = createCircularRefPairs(100); | |
echo $i . "," . memory_get_usage() . "\n"; | |
} |
CLIで実行時に php.ini の設定を一部変更して比較。この結果をグラフにしてみました。
循環参照GCを無効にすると、いちど確保されたメモリがいつまで経っても解放されないのがわかります。有効だと、だいたい4.5MBほど使ったところで循環参照がGCされて、650KBまで落ちています。
長時間かかるバッチ処理などで、複雑なOOPをすると参照カウンタが落ちないんじゃないかと心配する必要があったのは5.2までの話でした。今はもう忘れていいですよ、という確認でした。