GCWatcher:GCの起動とオブジェクトの回収状況を監視するための方法


GCを強制的に起動する方法について追記しました(4/8)


fladdictさんAS3でガベージコレクションを見張る画期的方法メモにて、


弱参照のDictionaryのオブジェクトキーを利用して、対象のオブジェクトがガベッジコレクションされたかどうかを見張ることができるんじゃね??

という面白そうなお題があがっていたので実装、検証を行ってみました。

検証のために実装したクラスGCWatcherには、次の二つの機能があります。

  1. GCが起動した場合にGARBAGE_COLLECTイベントを発行する
  2. 監視したオブジェクトを監視リストに追加し、GCに回収された場合にCHANGEイベントを発行する

このクラスを使うと、fladdictさんがエントリー中で書いていた、


onGarbageCollection イベントを発行できるし、DictionaryのキーをダンプすればGCされそこねたオブジェクトの一覧が取得できるんじゃないかと。

ということは実現できました。

しかし、オブジェクト一覧を取得できてもDisplayObject系のオブジェクトは強制的にメモリを解放する方法がないのでFlash PlayerのGCにいらついているFlasherの救済にはならないようです。

BitmapDataであれば、強制的に解放できるので利用価値があるかもしれないです。

実装方法

GARBAGE_COLLECTイベントの発行方法は、fladdictさんがエントリー中に書いていた方法の通りで、Dictionaryを弱参照で作成しTimerでオブジェクトがなくなっていないかを定期的にチェックしています。

また、この方法はid:nitoyonさんのブクマコメントにあったこの記事でも実装されています。

監視リストの作成も同様に弱参照のDictionaryを使って作成しています。

GCWatcherの使い方とサンプルなどソース一式は、このエントリーの最後に有ります。

次にGCWatcherを使ったサンプルを見ながら、GCの挙動を検証してみましょう。

BitmapDataのインスタンスを生成しつづけた場合のGCの挙動

このサンプルでは、BitmapDataを10x10の100個を50msごとに生成し、100msごとにGCの挙動をチェックしています。

画面左下に表示されるLeft/Watchedのうち、Leftが監視リストに残っているオブジェクトの個数です。

Watchedは前回のチェックから今回のチェックまでの間に監視していたオブジェクトの数です。(前回チェック時のLeft + 新規に追加されたオブジェクトの個数)

また、GCの起動を検出すると「GC!」と表示されます。監視リストが更新された場合は、「Changed!」と表示されます。

サンプルを眺めていると、GCがかなり高頻度で起動されているのがわかります。

また、毎回BitmapDataが回収されているのも確認できます。ただ、理論上Leftは100でなければならないのになぜか200で安定します。

これはGCWatcherの実装が問題なのか、それともGCが回収するタイミングの問題なのか詳細は不明です。

removeChild()をされたオブジェクトがGCに回収されるまでの挙動

次にDisplayObject系のオブジェクトがGCに回収される様子を観察するためのサンプルを紹介します。

このサンプルでは100個のSpriteを作成し画面にランダムな色と大きさの円を描いています。

すべてのSpriteは、GCWatcherの監視リストに追加されています。

また、表示された円をクリックするとイベントハンドラを残したままremoveChild()が呼び出されます。

このサンプルを動かして、円をクリックすると表示リストから外れるので表示が消えます。

しかし、左下の情報をみるとGCによってSpriteが回収されていないことが確認できます。

removeEventListener()を読んでないせいかと一瞬思うのですが、このままずっとサンプルを表示し続けると10分後くらいにGCによってSpriteが回収され左下の表示が変わります。

GCがどのタイミングでSpriteを回収するのかは、正直よくわからないのですが他のFlashを表示すると比較的すぐに回収されていくように感じます。

GCは相変わらず高頻度で起動されているので、この差は何なのか謎です。DisplayObjectあたりの実装に何か関係があるのかもしれないです。

GCWatcherの使い方

GCWatcherは、コンストラクタに更新確認する間隔をミリ秒で指定します。

watcher = new GCWatcher(100);
watcher.addEventListener(GCWatcher.GARBAGE_COLLECT, onGarbageCollect);
watcher.addEventListener(Event.CHANGE, onChange);			

また、監視リストに追加する場合はwatch()メソッドに渡します。

watcher.watch(bmpData);

今回のサンプルも含めてソース一式はここからダウンロードできます。

まとめ

fladdictさんのメモ先人の知恵にあるよう、Dictionaryの弱参照を使うとGCの起動を監視することができました。

ただ、BitmapDataなど一部のオブジェクト以外にはメモリ解放を強制させる方法がないのでFlash Playerのメモリ使用量が多くていらついているFlasherのエクスカリバーとはならないようです。

今後の課題

Tamarinで採用されているMMgcの挙動については、ここに載っているので今回作ったGCWatcherと会わせれば
もう少しGCの挙動を把握できるのではないかと思いました。ただ、アルゴリズム的な話になってくるので理解するのが面倒です。。。

それと、結局DisplayObject系のオブジェクトを監視してもメモリ解放できないので監視出来てもあまり役に立たないのではないかと思ってしまいました。

またBitmapDataを監視した場合は、監視リストから使用中と未使用のオブジェクトを判別するための効率的な方法を考える必要が有るかと思います。

オブジェクトの数がすくなければ線形探索でよいですが、まともにやるとO(n^2)なので微妙です。

追記(4/8):GCを強制的に起動する方法について

昔のブクマを見直してたらgskinner.com:gBlog: AS3: Resource Management pt 3という記事にGCを強制起動する方法が載っていました。この方法はオフィシャルで、サポートされていない方法なのであくまで開発時に使うようにして下さい。

この方法を追加した、2個目のサンプルを参考までにここにアップしておきます。