DOM オブジェクトとメモリリーク ― 2005年12月04日 20時21分
IE でのメモリリーク
ちょこちょこと紹介されているので知っている人も多いと思うが、IE には DOM ノードに絡んだメモリリークの問題がある。これに関しては Microsoft 自身の記事である「Understanding and Solving Internet Explorer Leak Patterns」に詳しいが、簡単にいえば DOM ノードオブジェクトに関する循環参照を作ると、IE を終了させるまでそのオブジェクトが解放されないというものだ。記事によればメモリリークには以下のようなパターンがあるという。
1. 単純な循環参照
ある DOM ノードオブジェクトのプロパティをたどっていくと自分自身に行き着く場合。以下のようなパターンが考えられる。
element.property == element
element1.property1 == element2, element2.property2 == element1
2. クロージャにまつわる循環参照
クロージャはそのクロージャが定義されたスコープへの参照を持っている。下の例でいうならば、element.onclick
にセットされているクロージャは、addClickAction
関数の引数である element
への参照を持っているということだ。また、element
のほうはというと onclick イベントにクロージャが結び付けられているため、element →クロージャ→ element という循環参照ができてしまっている。
function addClickAction(element)
{
element.onclick = function () { doSomething; };
}
3. 文書ツリーに属していない要素
これは循環参照とは異なる問題だが、IE では文書ツリーに属していない要素 (createElement()
で作成したあとどこにも挿入されていない要素) に対してイベントハンドラを結びつけることもメモリリークを招くらしい。もっともこの場合は先ほどと異なり、リークするメモリはほんの少し (たった数バイト) だそうだ。解決策としては要素を文書ツリーに追加してからイベントハンドラを結びつけることが挙げられる。
解決策
そして実際の対策であるが、1 は DOM ノードオブジェクトに直接 DOM ノードオブジェクトを結び付けないようにすればいいし、3 はよほど大量の作業をしない限りは影響が出ないだろうからいいとして (なくすにこしたことはないが) 、問題は 2 だ。JavaScript ではクロージャを使うことで柔軟な表現が可能になるので、それが使えないとなると困ってしまう。
そこで他の人がどうしているかを見ると、prototype.js ではイベントを結びつけるたびにその内容を記録し、文書が破棄されるとき (unload イベント発生時) に removeEventListener
または detachEvent
でそれらを解除するという方法をとっている。また、「Leak Free Javascript Closures」(「Collection & Copy」経由) では、クロージャを直接要素に結び付けるのでなく、そのクロージャを実行する関数を作りそれを要素に結びつけるようにしている。クロージャを実行する関数は実際のクロージャを取得するためのハッシュキーしか持っていないので参照が途切れるというわけだ。
なお、一見メモリリークを引き起こしそうだが実はそうでないパターンもある。以下の例がそうだ。
function addActions()
{
var links = document.getElementsByTagName("a");
for (var i = 0; i < links.length; i++)
links[i].onclick = function () { alert("Hello!"); };
}
クロージャが a 要素全体への参照を持っているように思えるが、links
の内容は動的 (あとから a 要素が追加 / 削除された場合、それは links
にも反映される) であり、links
が直接特定の a 要素への参照を持っているわけではないので循環参照にはならないと考えられる。
メモリリークが起こるわけ
さて、そもそもなぜメモリリークが起こるのかといえば、JavaScript / JScript と DOM ノードオブジェクトではガベージコレクタの仕組みが異なるからである。JavaScript / JScript では循環参照があってもメモリを解放してくれるのだが、DOM ノードオブジェクトは参照カウンタにより管理されているので循環参照があるとメモリが解放されずに残ってしまうのである。
そして、DOM ノードオブジェクトを参照カウンタで管理しているのは何も IE だけではない。Gecko (Firefox) も DOM ノードオブジェクトは XPCOM コンポーネントとして実装されているので、その管理は参照カウンタで行われている。ということは同じ問題は Gecko にもありそうだし、事実あったようだ。(修正されたのが最近なので Firefox 1.5 では修正されていない。)
その他のブラウザの実装がどうなっているかは知らないが、同じような実装である可能性は否定できないので、クロージャやイベントハンドラなどを組み合わせて使う場合はメモリリークに気をつけたほうがいいかもしれない。
とはいっても書いてる本人もよくわかってない部分が多いので (特に GC 周り) どしどし突っ込んでもらいたい次第。
それにしても MS の文書にはタスクマネージャを確認してなどと書いてあるけどそれでいいのだろうか? 中野さんはタスクマネージャを見ただけではメモリリークと断言できないといってるのだが。
参考
(本文中で触れたものは除く)
最近のコメント