GC判定再び

2008.12.20

GCされたかテストするクラスの改良版。

最後の参照を消すとき、

_instanceVar = null; //これでいつか解放されるはずだが・・

と書いているところを、

//常に戻り値のnullが代入されるが、同時に漏れを確認
_instanceVar = GC.shouldBeGarbage(_instanceVar); // 以降、ゴミとして回収されるべき!

と書くことにします[*注]。これで参照の消し忘れがあれば、直後にエラーになってくれます。多分。

ちなみにFlexでは子SWFがアンロードされると、以下のように表示されるのでこれの出番は少ないかもしれません。

[SWF のアンロード]Users:masuda:Documents:Flex Builder 3:Test:bin-debug:child.swf

以下にいくつかサンプルを挙げます。
ドキュメントクラスのコンストラクタに書かれているコードだと思って下さい。

ローカル変数はGCされる:

var o:DisplayObject = new Sprite();
GC.shouldBeGarbage(o); //=> GC succeeded: 4489216 => 4771840

インスタンス変数に退避したため延命され、GCされない:

var o:DisplayObject = new Sprite();
_instanceVar = o;
GC.shouldBeGarbage(o); //=> Error

表示リストに残っているため、GCされない:

_instanceVar = new Sprite();
addChild(_instanceVar);
//removeChild(_instanceVar); //この行が必要だった
_instanceVar = GC.shouldBeGarbage(_instanceVar); //=> Error

ローカル変数はクロージャに延命され、クロージャはstageに延命されるのでGCされない:

var o:DisplayObject = new Sprite();
addChild(o);
removeChild(o);
GC.shouldBeGarbage(o); //=> Error
//o = null; //この行が必要だった
stage.addEventListener(
  Event.ENTER_FRAME,
  function(evt:Event):void {/*どの変数も参照してなくても害*/}
);

クロージャとメモリリークについての親切な解説はこちら:
http://www.imajuk.com/blog/archives/2008/04/post_3.html

以下、ソースコードです:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;
  import flash.system.System;
  import flash.utils.Dictionary;

  public class GC {
    private static var sprite:Sprite = new Sprite();

    public static function start():void {
      try {
        new LocalConnection().connect('foo');
        new LocalConnection().connect('foo');
      } catch (e:*) {}
    }

    public static function shouldBeGarbage(value:Object):* {
      try {
        //テストは1フレ後だが、この時点のスタックトレースがほしい
        throw new Error('Probably, it is not garbage:' + value);
      } catch (e:Error) {
        var dict:Dictionary = new Dictionary(true);
        dict[value] = true;
        value = null;

        var before:uint = System.totalMemory;
        GC.start();

        var err:Error = e;
        sprite.addEventListener(Event.ENTER_FRAME, function(evt:Event):void {
          evt.target.removeEventListener(evt.type, arguments.callee);
          for (var key:Object in dict) throw err;
          trace('GC succeeded:', before, '=>', System.totalMemory);
        });
      }
      return null;
    }
  }
}

[*注] 以下の例は、全ての参照を消すのに先だってshouldBeGarbageを行うためGCが成功しそうにありませんが、実際のところ、このように書いてもテストをパスします。

GC.shouldBeGarbage(_instanceVar);
_instanceVar = null;

恐らく、LocalConnectionによる強制GCの発動は即座に行われずに、一度Flash Playerに制御を戻した後行われているのだと想像しています・・。そうなると、shouldBeGarbage(x)の意味合いとしては、「現在実行中のコードを抜けた後には、xはゴミとなり回収されるべきだ」といったところでしょうか。