[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