メモリーリークパターンを理解する
メモリーリークに関する覚え書き
- メモリーリークとは
- コンピュータの動作中に、使用可能なメモリ容量が、だんだん減っていく現象。
- OSやアプリケーションソフトが、処理のために占有したメモリ領域を、解放しないまま放置してしまうために起きる。
- メモリーリークパターン(以下のタイプを考察)
ドキュメントツリーに属さないノードに、イベントを貼り付けた場合
- ページを破棄するときに、ドキュメントツリーのノードをたどってイベントを切り離されるが、それに属していないと、削除されない。
- リークするメモリは数バイト程度なので、少量なら無視できる
//createElement で作ったノードに、イベントを貼り付けただけのコード function hoge ( ) { var e = document.createElement( 'div' ); e.onclick = function () { ; }; }
DOM プロセッサが管理しているオブジェクトと、
スクリプトエンジンが管理しているオブジェクトが違う場合
// ノードのオリジナルなプロパティに、オブジェクトを追加するコード var e = document.getElementById( 'hoge' ); e.fuga = function ( ) { ; };
- 変数 (e) の参照元( hoge )は、 DOM プロセッサが管理しているオブジェクトで、それに付け足した .fuga は、スクリプトエンジンが管理しているオブジェクトである。
これが、メモリーリークを招くパターンである
(変数に代入されたノードに、勝手なプロパティを付けてはならない。)
- 一見、循環しているようでも、スクリプトエンジンが管理しているオブジェクトなので、リークしない。
(obj.fugaには、objの参照アドレスが保存されるだけだろうから、と勝手な思い込み)
// スクリプトエンジンが管理しているオブジェクトが循環するコード var obj = new Object; obj.fuga = obj;
いわゆるクロージャが問題になるパターン
- クロージャになるコード1
function hoge ( id ) { var e = document.getElementById( id ); e.onclick = function ( evt ) { alert( e.id ); // 変数 e を保持 }; }
- クロージャになるコード2(以後、理解しやすいのでこちらを使う)
function hoge ( id ) { var e = document.getElementById( id ); var f = function ( evt ) { alert( e.id ); }; e.onclick = f; }
どのようなことがおこって、問題になるのか?
- メモリーリークは、循環参照しているオブジェクトの管轄が異なる場合に発生する。
つまり、DOM プロセッサが管理しているオブジェクトから、スクリプトエンジンが管理しているオブジェクトに、そしてまた DOM プロセッサが管理しているオブジェクトにと、循環参照が成立した時にリークする。
(オブジェクトの管理者が同じなら、循環参照しても問題はない)
(function outerFunc() { var e = document.getElementById( 'hoge' ); var innerFunc = function ( evt ) { alert( e.nodeName ); }; e.onclick = innerFunc; })();
-
- e(DOM) → onclick(DOM) → Function(script) → e(DOM)と、
管轄が異なるオブジェクトの循環参照が成立する。 - 色で表現すれば、青赤青となったときに、循環参照する。(メモリーリークとなる)
- e(DOM) → onclick(DOM) → Function(script) → e(DOM)と、
- 一見すると循環参照していないようなので、勘違いしてしまう場合
(function outerFunc() { var e = document.getElementById( 'hoge' ); var innerFunc = function ( evt ) { var element = evt.target; alert( element.nodeName ); }; e.onclick = innerFunc; })();
-
- 関数 (outerFunc) の変数 (e) は、 onclick のイベントが発生すると、関数 (innerFunc) が実行されるようになっている。しかし、そこには、外側で使われている変数 (e)が使われていない。
「だから循環参照になっていない」と考えるのは、間違いである。
- 関数 (outerFunc) の変数 (e) は、 onclick のイベントが発生すると、関数 (innerFunc) が実行されるようになっている。しかし、そこには、外側で使われている変数 (e)が使われていない。
-
- 関数 (innerFunc) の element と 関数 (outerFunc) の変数 (e)は、同じノード(hoge)を参照している。なのでこれが循環参照でリークしている考えるのも間違いである。
-
- いわゆるクロージャを生成した時点で、関数 (innerFunc) は、変数 (e) を、いつでも参照できるようになっている。なので循環参照が成立する。
-
- 色で表現すると。
e(DOM) → onclick(DOM) → innerFunc(script) → e(DOM) が無くても参照可能になっている!
- 色で表現すると。
- メモリーリークを断ち切ってしまう
(function outerFunc() { var e = document.getElementById( 'hoge' ); var innerFunc = function ( evt ) { alert( e.nodeName ); }; e.onclick = innerFunc; e = null; // これをスクリプトが管理しているオブジェクトにしてしまう })();
-
- もう解説は、いらないだろう。
- 以下は、リークしない
(function outerFunc() { var e = 'hoge'; var innerFunc = function ( evt ) { var element = document.getElementById( e ); alert( element.nodeName ); }; document.getElementById( e ).onclick = innerFunc; })();
-
- document.getElementById( e ) の、onclick で呼ばれる、関数 (innerFunc) から参照できる変数 (e) は、スクリプトエンジンが管理しているオブジェクトである。
-
- 色で表現すると、
document.getElementById( e ) → onclick(DOM) → innerFunc(script) → e(DOM) が無くても参照できる!
- 一見すると循環参照していないようなので、勘違いしてしまう場合 その2
(function outerFunc() { var e = 'hoge'; var d = document; //←これを追加 var innerFunc = function ( evt ) { var element = document.getElementById( e ); alert( element.nodeName ); }; document.getElementById( e ).onclick = innerFunc; })();
-
- 色で表現すると。 document.getElementById( e ) → onclick(DOM) → innerFunc(script) → d(DOM)
- 下記はメモリリークパターンに該当しない
function hoge() { document.getElementById('hoge').onclick = function(e) { alert( 'abc' ); }; }
-
- 外側に参照可能な、DOM プロセッサが管理がしているローカル変数がない
- 下記も同様。
function hoge() { var nodes = document.getElementsByTagName('A'); var I = nodes.length; var i; for (i = 0; i < I; i++) { nodes[i].onclick = function() { // 変数 nodes、I、i を保持 }; } }
-
- まだまだ続く予定
結局これがしたいのだが
- これはまだ、書きかけです
- 下は何がしたいのかというと、イベントを貼り付ける node の親からのitem番号でそのnodeを
見つけて、その生きたリストに貼り付ける。それならメモリーリークしない!?
- あぁ〜この疑問符がとれるのは、いつのことやら・・・
function addEvent ( element, evt, eventHandler, flag ) { var cnt = 0, n = element, t; if( element != document ) { while( n = n.previousSibling ) cnt++; t = element.parentNode; t.item( cnt )./*@if( @_jscript ) attachEvent( 'on' + @else@*/ addEventListener( /*@end@*/ evt, eventHandler, flag ); } }