言語の 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 の恩恵を受けるためには、いくつか特別な記述が必要なみたいだ。