[ActionScript 3.0] クロージャが原因のメモリリーク

クロージャを利用した場合、それを内包する関数(メソッド)におけるローカル変数の状態のまとまりを、そのクロージャが実際に呼び出されたときのために、スクリプトエンジン(Flash PlayerやAIR)が保持しつづける。

そのため、ローカル変数のオブジェクトは関数終了後も破棄されず、クロージャを保持しているオブジェクトが破棄されないかぎり、ずっとメモリを使用したままになる。

public class TestClosureMem extends Sprite {
     
    private var sampleTimer:Timer = new Timer(50, 20);
    private var checkTimer:Timer = new Timer(200);
    private var fnVec:Vector.<function> = new Vector.</function><function>;
     
    public function TestClosureMem() {
        sampleTimer.addEventListener(TimerEvent.TIMER, doSample);
        sampleTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onComplete);
        checkTimer.addEventListener(TimerEvent.TIMER, checkMemory);
         
        sampleTimer.start();
        checkTimer.start();
    }
     
    private function checkMemory(e:TimerEvent):void {
        trace(System.totalMemory);
    }
     
    private function doSample(e:TimerEvent):void {
        var bmpData:BitmapData = new BitmapData(1000, 1000);
         
        fnVec.push(
            function ():void {
                // bmpDataをここで使わなくても、そのデータは保持されつづける
            }
        );
    }
     
    private function onComplete(e:TimerEvent):void {
        // ここでfnVecを解放しないと大変なことになる。
        // OSが不安定になることもあるので、テストのときは要注意。
        fnVec = null;
 
        trace("--- closure cleared ---")
         
        fnVec = new Vector.</function><function>;
        sampleTimer.start();
    }
     
}

この例の場合、クロージャ(無名関数)をfnVecというVectorインスタンスが保持しているから、それが破棄されないかぎりメモリは増えつづけていく。

stageからの参照のつながり(スコープチェーン)は、以下のようになっている。

stage - TestClosureMem - fnVec - 各クロージャ

よって、TestClosureMemインスタンスかfnVecがガベージコレクション(GC)の対象にならないかぎり、bmpDataは破棄されずにどんどんメモリを占有していく。

GCの仕組みがある言語でも、メモリの問題からは逃れられない。そこにクロージャがからんでくるとさらに厄介なことになるので、特段の理由がないならばあまりうかつにクロージャを使わないほうがいいのかもしれない。

ActionScript 3.0

Posted by takasho