Flash/Flex のアプリケーションを開発していると (たまには) メモリリークとか気になることもあるかと思います。今回は Flash Player 9 のガーベジコレクションについてです。詳細な実装レベルだと、Flash Player のアップデート時に、ガーベジコレクション周りでも多少の変更が行われたりしていますが、いまのところ基本的なモデルは変わっていません。今回はこの "基本的なモデル" を紹介します。
Flash Player 8 以前は少し違ったモデルになっています。以下の記述内では Flash Player とあったら Flash Player 9 のことだと思ってください。
Flash Player のメモリ確保
Flash Player のメモリ確保時の動作は特にユニークなものではありません。が、話の基本になるのでとりあえず以下概要です。
メモリの確保は負荷の高い部類に属する処理です。そのため、Flash Player がメモリを確保するときは、必要になるたびに毎回確保するのではなく、一度にある程度大きなメモリ領域を確保しておいて、それを小さなブロックに分けて使用します。メモリの割り当てが必要になるごとにブロックを一つ一つ使用していきますが、これらのブロックは既にメモリ上に確保されています。すなわち、メモリの割り当てを行うけれども、実際のメモリの確保は必要は無い状態になるわけです。全てのブロックを使い切ると、次の大きなメモリ領域を確保します。
ちょっと、"たんす" と "引き出し" の関係に似ています。買うのは "たんす" 単位、使うのは "引き出し" 単位、ということで。
なお、ビットマップデータのように大きなデータは個別にメモリの確保が行われます。
メモリ参照の削除
さて、今度は一旦確保したメモリ領域を開放するときの動きです。
例として下のコードを考えることにしましょう。
var foo = new Foo(); foo = null;
まず、一行目の右辺でオブジェクトを生成しています。その際に、新規オブジェクトに対してメモリ領域が割り当てられます。
以降の話を単純化するため、ここでは
「一つのオブジェクトに割り当てられる領域 = ブロック (引き出し) 一つ」 と仮定
することにします。そうすると一行目では新規オブジェクトの格納されている "引き出し" への参照を foo という変数に渡していることになります。
二行目で foo の持つ参照を無効にしています。このとき削除されているのは "引き出し" へ の参照であって "引き出し" の中身ではないことは重要です。つまり、このままだと "引き出し" は空になっていないはずです。変数に null を代入するだけではメモリを開放するのに十分ではないわけです。
ところが、プログラム側からはこれ以上メモリの開放に関与することはできません。後は、どこかのタイミングで起動されるはずのガーベジコレクタに任せることになります。
ガーベジコレクションの動き
Flash Player 上ではオブジェクトの格納されている "引き出し" はツリーとして管理されています。ツリーのルートは Stage です。(正確にはクラス定義とローカル変数がルートのツリーもあります)
ガーベジコレクタは、Stage から順番にツリー上のノード (ここでは "引き出し" ) を辿ります。途中で見つかったノードには印を付けます。
上のサンプルコードに当てはめてみると、一行目の実行が完了した時点では foo から新規オブジェクトへの参照が存在します。ですので、このタイミングでガーベジコレクタが実行されると (Stage から foo を含むオブジェクトへたどり着くことができれば) foo を含むオブジェクトに印を付けた後に新規オブジェクトにも印を付けます。
ところが、二行目 (foo = null) の実行後には新規オブジェクトへの参照は削除されています。そのため、この時点でガーベジコレクションが実行されると foo を含むオブジェクトに印を付けた後、新規オブジェクトへとたどることができません。その結果、新規オブジェクトには印を付けません。
ガーベジコレクタがツリーを最後まで全て辿り終えて、それでも印の付いていない "引き出し" は、ようやく再利用や開放の対象になります。この後実際にメモリを開放するのもガーベジコレクタの仕事です。
さて、ガーベジコレクタが動いても実際のメモリ開放までは行われないケースがあります。以下、3 点ほど理由を説明します。
重要な点ひとつ目
まず、メモリの割り当てや解除はブロック単位で行われますが、物理的なメモリの確保や開放は、ブロックの集合であるもっと大きな塊が単位です。前にも書いたように、使うのは "引き出し" 単位、売ったり買ったりは "たんす" 単位、ということです。
このため、たとえ一つでも "引き出し" が使用中だと、その "たんす" は返却できません。実際に、ガーベジコレクタが実行された結果、いくつかの "引き出し" を空にしていたとしても、 "たんす" が返却できなければ、確保しているメモリの量は減らないわけです。
このような場合にはガーベジコレクタが使用中の "引き出し" を他の "たんす" に移すことで、全ての "引き出し" を空にして (いわゆるコンパクション) "たんす" を開放できるようにします。が、これは必ず実行されるわけではありません。その理由は 2 つめの理由にあります。
重要な点ふたつ目
Flash Player のガーベジコレクタは控えめで、メモリの開放を積極的に行うようにはなっていません。
ガーベジコレクションは非常多くの作業を行うため、長時間に渡ってリソースを占有する可能性があります。その結果、例えばちょっとの間なり画面が固まった状態になってしまうかもしれません。これはユーザインターフェースとしては望ましくないことです。
そこで、ガーベジコレクタは全ての処理が完了していなくても、描画処理やインタラクションを邪魔しないように途中で実行を中断することがあります。コンパクションのように重い処理は、より完了しない可能性が高いと思われます。
これは、また、描画処理やスクリプトの実行でクライアントの負荷が高い状況が続いていると、メモリの開放が起きにくいということでもありますね。
重要な点みっつ目
繰り返しになりますが、ガーベジコレクションは重い処理です。そのため、起動される条件が限定されています。それは、
- 新しくメモリの割り当てが要求された
- 空いているブロックの残りが少ない
の2つが重なって、新しくメモリを確保する必要が出そうな場合です。
ということは、スクリプトが何も実行されていない状況では、ガーベジコレクタも起動されません。アイドル状態のアプリケーションを一生懸命観察していてもメモリは減らない訳です。
いつも勉強させて頂いております。
興味本位でコメントさせて頂きますが、delete で
削除した場合、『引き出し』はなくなるのでしょうか?
やはりnullの場合と同様に残っているのですか?
var foo = new Foo();
delete foo;
ben さん、こんにちは。
ナイスな突っ込みありがとうございます。
delete を使用すると変数定義が削除されます。
今回の例であれば foo という変数自体が無かったことになります。
結果として参照もなくなります。
”引き出し” はそのままですね。
null の場合は値が変わるだけで、foo を参照することは引き続き可能です。