都合で eval() を動的にバカスカ使いまくるコードを書いてた時に気づいたことです。

※この記事は2018年2月27日に初版を書きました。Chromeのバージョンは失念しましたが、当時の最新だったと思います。


たまたま、そのあとChromeの開発者ツールでメモリリークの確認してました。方法は、メモリ開放→確保できるという想定の操作を何十回か繰り返して、その前後のスナップショットを見比べるというものでしたが……。

実行前
実行前
実行後
実行後

なんか異様に『(string)』がメモリ食ってる。『(compiled code)』は eval() してるから分かるんだけど。

疑問に思って内訳を見たら、どうも eval() に渡したと思しき文字列がびっしり出てきました。そんなのどこにも保持してないはずですが、なんで誰かに参照されてるんだろう。もしかしてひとつも解放されずに残ってるんだろうか。

というわけで以下のようなページを作って確認しました。なお eval() の中身を _a = _str にしてないのは、長い文字列じゃないとツール上で探すのが大変だからです。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8" />
	<script>

window.addEventListener('load', function() {
	document.getElementById('kick-button').addEventListener('click', kick);
	document.getElementById('destroy-button').addEventListener('click', destroy);
});

var _str = new Array(1000).join('!'); // !マークいっぱい
var _a;

var kick = function() {
	eval("_a = '"+ _str + "'");
}

var destroy = function() {
	// 参照を切る -> 消えない
	_a = _str = null;
	
	// 呼び元を切る -> 消えない
	document.getElementById('kick-button').removeEventListener('click', kick);
	
	// 関数自体を消す -> 消えない
	kick = null;
}

	</script>
</head>
<body>
	<button id="kick-button" type="button">kick</button>
	<button id="destroy-button" type="button">destroy</button>
</body>
</html>

まず上記のページを開いて、何かする前にスナップショットを撮ります。

開始時点の状態
開始時点の状態

もちろんメモリ上の文字列のところには、_str に格納されているビックリマークが出てきます。

次に eval() が流れる『kick』ボタンを5回押して、もう一度スナップショットを撮ってみると……。

kick後の状態
kick後の状態

やっぱり eval() に渡したコードが全部出てきました。ビックリマークが2行になってるのは、前述の理由で _a に直接 _str をセットせずに、別の文字列としてセットしたからですね。

参照元とされている『@852463』は、上の『(compiled code)』の中を探したら出てきました。中身はこんな感じです。

@852463の中身
@852463の中身

最後に、メモリ開放できるはずの『destroy』ボタンを押して、もう一度スナップショットを撮ってみます。

destroy後の状態
destroy後の状態

_a_str は解放されたけど、eval() に使った文字列は解放されてません。イベントリスナーどころか kick() まで消したのに。

でも考えてみると、たぶん『(compiled code)』に出てくるということは、文字列がソースコードの一部として扱われているということですよね。確かにこれがメモリ解放されるなら、「eval() を使えばソースコードが確保してるメモリさえ節約できる」ことになってしまい、ズルいのかな。

解放されると思い込んでた私のほうが空回りしてただけかもしれません。いや、そもそも eval() をバカスカ使いまくるというのが……?