2010年6月28日月曜日

IEのメモリリーク問題

前に書いた javascript のクロージャと ローカル変数 に関連するメモ。
というか、これを書くための前置きとして書いたものだったり。




もう話題としては「かなり」古いんだけど、IE6以前のJavascriptにはメモリリークの問題があって
[IE] Javascript で動的にイベントを登録するとメモリリークが発生 (support.microsoft.com)
Internet Explorer リーク パターンを理解して解決する (msdn.microsoft.com)
この辺を参照かな。
IE6とかさっさと滅べばいいのに。

(A) たとえば、このコードはメモリリークを起こします。
function foo(element)
{
    element.onmousedown = function(){
        alert('test');
    };
}

(B) これも起こします。
function foo()
{
    var element = document.getElementById('foo');
    element.onmousedown = function(){
        alert('test');
    };
}

(C) でもこれは起こしません。
function foo()
{
    document.getElementById('foo').onmousedown = function(){
        alert('test');
    };
}
なぜでしょう?

これは、Javasciprtのガベージコレクションは「マーク&スウィープ方式」が使われているのですが、DOMオブジェクトだけは内部で「参照カウンタ方式」を使っているために起きている問題です。DOMオブジェクトがJavaSciprtオブジェクトを参照していて、そのJavascriptオブジェクトでもDOMオブジェクトを参照していたりすると、循環参照になってメモリから解放されずに残ってしまうわけです。

つまり上の例(A)(B)だと、DOMオブジェクト「element」が onmousemoveハンドラでクロージャfunction(){ … }を参照していて、クロージャでも内部でelementを参照しているため、メモリリークが発生してしまいます。
一見、このクロージャはelementオブジェクトを参照してないように見えますが、javascriptのクロージャとローカル変数で書いたように、クロージャ内側でelementを使っていようがいまいが、同じスコープで宣言されている全てのローカル変数をクロージャ内に保持してしまう特性上、elementもきっちり参照しています。

(C)の例では、DOMオブジェクトをローカル変数として定義していないのでクロージャ内でid='foo'のDOMオブジェクトは参照されていません。したがってメモリリークは発生しません。


function foo()
{
    var element = document.getElementById('foo');
    element.onmousedown = function(){
        alert('test');
    };
    element = null;
}
このようにelementをnullで消しておけば、参照が切れてメモリリークを防げます。


elementを戻り値として返したい場合などは、try{ … } finally { … }を使うとスマートに書けたりします。
function createButton()
{
    element = document.createElement('foo');
    element.innerHTML = 'click here';
    element.onclick = function() {
        alert('click!');
    };

    try {
        return element;
    }
    finally {
        element = null;
    }
}

document.getElementById('hoge').appendChild( createButton() );

PerlやPythonから入ってる人は、なぜ(A)(B)が循環参照になるのか理解しづらいかもなので、書いてみました。
なんか他にも書いておきたいことあったけど、長くなりそうだからもういいや。

0 件のコメント:

コメントを投稿