言語の GC 機能と参照カウント (中編)
前編でブクマコメントや日記コメントで
- どうしたら循環参照によるリークを回避できるのかを説明してほしい
- PHP 5.3 で cycle collector が搭載されることについてのフォローが欲しい
という意見があったので、それも順次取り上げていくことにする。
本当はこれを後編としたかったんだけど、どんどん詰め込みたい内容が増えているので、勝手ながら一旦切って中編、後編の 2 本立てとさせてもらおう。
SpiderMonkey と XPCOM のガーベジコレクション
さて、cycle collector というと Firefox 3 から搭載されるということで気になっている人も多いと思われるので、まずは SpiderMonkey (JavaScript の Mozilla での実装) ではどのような GC (ガーベジコレクション) が行われるのかを見てみたい。
基本的に、SpiderMonkey は Ruby と同じくマーク & スイープな GC を行う。次のようなコードを xpcshell (xulrunner などに付属*1 ) で走らせると次のような結果になる。
function make_circular() { var foo = {}; var bar = {}; var baz = {}; foo['bar'] = foo; bar['baz'] = baz; baz['foo'] = foo; return foo; } for (var i = 0; i < 1000000; ++i) { make_circular(); }
% run-mozilla.sh xpcshell test.js & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done [1] 12801 6733 6798 6897 6897 7007 7860 7926 ... 11381 11414 11547 11646 11680 11713 11779 11845 11879 11947 11947 11947 ... (以下同じ数値)
何らかの閾値を越えたところで GC が動作し、メモリ使用量が一定に保たれることがわかる。
このように、Plain-Old JavaScript Object (これも POJO じゃん) は、正しく GC されるのだけど、 XPCOM *2 オブジェクトを JavaScript で操作するケースではまた違った問題が発生する。
「Using XPCOM in JavaScript without leaking」という秀逸な記事にあるように、XPCOM では、多くのコンポーネントフレームワーク同様、参照カウントベースの GC を採用している。もし、XPCOM のコンポーネントとして実装されているコンテナに、JavaScript のオブジェクトへの参照を突っ込んで循環参照を作ったらどうなるだろうか。もちろん、せっかくのマーク & スイープ GC は効かなくなり、リークが発生してしまうだろう。SpiderMonkey 側のガーベジコレクタは、(JavaScript のオブジェクトとして) 扱っている XPCOM オブジェクトがそれ以外 (つまり XPCOM を利用する SpiderMonkey 以外の) モジュールから参照されていないことを確認しようがないからだ。
で、これが本当かどうか、実際に試してみる。
var nsIMutableArray = Components.interfaces.nsIMutableArray; var impl_nsIMutableArray = Components.classes['@mozilla.org/array;1']; function create_nsIMutableArray() { return impl_nsIMutableArray.createInstance().QueryInterface(nsIMutableArray); } function make_circular() { var foo = create_nsIMutableArray(); var bar = create_nsIMutableArray(); var baz = create_nsIMutableArray(); foo.appendElement(bar, false); bar.appendElement(baz, false); baz.appendElement(foo, false); return foo; } for (var i = 0; i < 1000000; ++i) { make_circular(); // if (!(i % 100000)) gc(); // <- これは、あってもなくてもいい }
XPCOM には、スクリプト中で使える (scriptable) な適当なディクショナリがなかったので*3、nsMutableArray を使った。これを Mozilla 1.8 系の SpiderMonkey + XPConnect + XPCOM で走らせると次のような結果になる。
6470 6798 6930 6963 7003 7309 7361 ... 36335 36370 36403 36403 36436 36470 36470 36503 ... (延々増え続ける)
まあ、というわけで、cycle collector の出番ということになる。*4
先のプログラムを Mozilla 1.9 系のブランチの xpcshell で走らせると、次のような結果になった。なお、この xpcshell はXULRunner の nightly buildに含まれているものだ。
% ./run-mozilla.sh ./xpcshell /tmp/test2.js & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done [2] 13900 911 913 913 ... (以後、ずっと 913)
余談だけど、心なしかこちらの方が圧倒的にメモリのフットプリントが小さくなっている気もする。そもそも前提となるコンフィギュレーションが違う可能性があるので何ともいえないが、もしその通りならうれしい話だ。
なお、"@mozilla.org/array;1" が cycle collector に対応している様は、mozilla/source/xpcom/ds/nsArray.cpp で見ることができる。次の箇所がそれだ。
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsArray) NS_INTERFACE_MAP_ENTRY(nsIArray) NS_INTERFACE_MAP_ENTRY(nsIMutableArray) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMutableArray) NS_INTERFACE_MAP_END ... NS_IMPL_CYCLE_COLLECTING_ADDREF(nsArray) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsArray) NS_IMPL_CYCLE_COLLECTION_CLASS(nsArray) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsArray) tmp->Clear(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsArray) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mArray) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END||<
PHP 5.3 に搭載された cycle collector
PHP 5.3 そのもののハイライトについては、まったりと、かつての俺の天敵の一人だったIlia Alshanetsky氏の「Introduction to PHP 5.3」を読んでもらうとして、PHP の cycle collector についてちょっと降れておく。
そもそも PHP に cycle collector を実装する試みは 2007 年度の Google SOC のidea の一つとして David Wang により行われたものだ。2007/10/7 にパッチとして php-internals に投稿され、いくらかの修正の後マージされた。
さて、前編で使ったソースコードを PHP 5.3 で走らせてみると...?
array(1) refcount(1){ ["bar"]=> array(1) refcount(2){ ["baz"]=> array(1) refcount(1){ ["foo"]=> &array(1) refcount(2){ ["bar"]=> array(1) refcount(2){ ["baz"]=> array(1) refcount(1){ ["foo"]=> &array(1) refcount(2){ ["bar"]=> *RECURSION* } } } } } } } 322776 323268 323760 324252 324744 ... (以下単調増加していく) 1958240 1958732 1959224 1959716 1960208 1960700 1961192 452936 (GC が行われ、急激にメモリ使用量が減る) 453004 453088 453172 453256 453340 ...
何らかの閾値に達したところで、GC が動作してオブジェクトが開放されている。この動作は、スクリプトを走らせている間、ずっと繰り返される。
以上のスクリプトでは、暗黙的に GC を動作させていたが、新たに追加された組み込み関数 gc_collect_cycles()
を呼び出すことで、意識的に GC を動作させることもできる。
また、暗黙的な GC の動作を停止 / 開始させたい場合は、それぞれ gc_disable()
/ gc_enable()
を使う。現在 GC が有効かどうかは gc_enabled()
で取得可能。
次回は cycle collector の実装と、回避方法について書く予定。
*1:ちなみに Ubuntu や Debian とかだと /usr/lib/xulrunner/xpcshell
*2:XPCOM (Cross Platform COM) とは Firefox など Mozilla 製品で使われているコンポーネント技術で、JavaScript からは XPConnect というよくできたブリッジによって、シームレスに XPCOM オブジェクトの利用が可能になっている
*3:nsIDictionary というインターフェイスはあるが実装がない
*4:JavaScript から使う分には関係ない話なんだけど、「Interfacing with the XPCOM cycle collector」で説明されているように、あるコンテナオブジェクトの実装で cycle collector の恩恵を受けるためには、いくつか特別な記述が必要なみたいだ。