目次


JavaScript と Dojo によってブラウザーで発生するメモリー・リークを発見し、解決する

sIEve を利用してスキャンし、厳密に調べる

Comments

はじめに

一般的に、Web アプリケーションではブラウザーのメモリー・リークは問題になりません。ユーザーはページ間をナビゲートし、ページを切り換えるたびにブラウザーはリフレッシュされます。1 つのページでメモリー・リークがあったとしても、ページが切り換えられた後に、そのリークは解放されます。リークの規模はわずかであるため、通常は無視されます。

Ajax 技術が導入されると、メモリー・リークが問題になってきました。Web 2.0 スタイルのページでは、ユーザーはあまり頻繁にページを更新しません。Ajax 技術を使用すると、ページのコンテンツは非同期に更新されます。極端な状況では、Web アプリケーション全体が 1 つのページ上に構築されています。その場合には、リークは累積され、無視することができません。

この記事では、メモリー・リークが発生するメカニズムと、sIEve を使用してリークの原因を見つける方法について学びます。メモリー・リークの問題を詳しく探る上では、実用的な例による問題と解決策が役に立つはずです。この記事のサンプルのソース・コードはダウンロードすることができます。

この記事を理解する上で、JavaScript と Dojo Toolkit を使用した経験があると役立ちますが、それが必ず必要なわけではありません。

リークのパターン

Web 開発者に知られているとおり、IE (Internet Explorer) は Firefox やその他のブラウザーとは異なります。この記事で説明するメモリー・リークのパターンと問題は主に IE を対象としていますが、IE のみを対象としているわけではありません。適切なプラクティスは、どのブラウザーにも適用することができます。

JavaScript が持つ性質や、JavaScript オブジェクトと DOM オブジェクトに対するブラウザーのメモリー管理が原因となり、不注意にコーディングされた JavaScript によってブラウザーのメモリー・リークが発生します。メモリー・リークを引き起こすパターンとして、以下の 2 つがよく知られています。

循環参照
ほとんどすべてのリークの根本原因は循環参照です。一般的に、IE は JavaScript のおける循環参照を適切に処理、解消することができます。例外は DOM オブジェクトが導入されている場合です。JavaScript のオブジェクトが DOM 要素を参照している一方で、参照されている DOM 要素のプロパティーにはその JavaScript オブジェクトが指定されている場合、循環参照が発生し、DOM ノードがリークします。リスト 1 に、メモリー・リークを扱った記事でこの問題の具体的な例を説明するためによく使われるコード・サンプルを示します。
リスト 1. 循環参照によるリーク
var obj = document.getElementById("someLeakingDIV");
document.getElementById("someLeakingDiv").expandoProperty = obj;

この問題を解決するためには、文書からノードを削除する準備が整った時点で、expandoProperty を明示的にヌルに設定します。

クロージャー
気付かないうちにクロージャーによって循環参照が作られるため、メモリー・リークが発生します。親関数の変数は、クロージャーが存続する限り保持されます。クロージャーは親関数のスコープの外でも参照されるため、注意深く扱わないとメモリー・リークが発生します。リスト 2 はクロージャーによって引き起こされるリークを示していますが、これは JavaScript のコーディング・スタイルとして一般的なものです。
リスト 2. リークを引き起こすクロージャー
         <html>
<head>
<script type="text/javascript">
window.onload = function() {
var obj = document.getElementById("element");
    // this creates a closure over "element"
    // and will leak if not handled properly.
    obj.onclick = function(evt) {
        alert("leak the element DIV");
    };
};
</script>
</head>
<body>
<div id="element">Leaking DIV</div>
</body>
</html>

孤児ノードとメモリー・リークを検出するツールである sIEve を使用すると、DIV 要素が 2 度参照されていることがわかります。1 つの参照はクロージャー (onclick イベントに割り当てられた匿名関数) によって保持されており、ノードを削除したとしてもこの参照が削除されることはありません。あとでアプリケーションが element ノードを削除しても、JavaScript による参照は相変わらず孤児ノードを保持することになります。この孤児ノードによってメモリー・リークが発生します。

なぜクロージャーによって循環参照が発生するのかを理解することが重要です。「Memory Leakage in Internet Explorer - revisited」という記事の図には、この問題が明確に説明されています。この図を図 1 に示します。

この問題を解決するための 1 つの方法は、クロージャーを削除することです。

図 1. DOM と JavaScript との間に循環参照を作り出すクロージャー
DOM と JavaScript との間に循環参照を作り出すクロージャー
DOM と JavaScript との間に循環参照を作り出すクロージャー

sIEve の紹介

sIEve はメモリー・リークの検出を支援するツールです。sIEve のダウンロードとドキュメントについては「参考文献」を参照してください。sIEve のメイン・ウィンドウを図 2 に示します。

図 2. sIEve のメイン・ウィンドウ
sIEve のメイン・ウィンドウ
sIEve のメイン・ウィンドウ

Show in use (使用状況を表示)」をクリックすると、非常に有用な情報が表示されるようになります。使用されている DOM ノードがすべて表示され、孤児ノードであることや、DOM ノードへの参照の増加と減少も表示されます。

図 3 はビューの例を示しています。リークの原因は以下のとおりです。

  • 孤児ノードが存在すること (「Orphan (孤児)」列に Yes が表示されています)
  • DOM ノードへの参照が不適切に増加していること (青で表示されています)

sIEve を使用すると、リークしているノードを見つけて、それらのノードを修正するためのコードを検討することができます。

図 3. sIEve の「DOM Nodes in use (使用中の DOM ノード)」ビュー
sIEve の「DOM Nodes in use (使用中の DOM ノード)」ビュー
sIEve の「DOM Nodes in use (使用中の DOM ノード)」ビュー

リークしているノードを sIEve によって検出する

リークしているノードを検出するためには以下のステップに従います。

  1. Web アプリケーションの URL を指定して sIEve を起動します。
  2. Scan Now (今すぐスキャン)」をクリックし、現在の文書で使用されているすべての DOM ノードを検出します (オプション)。
  3. Show in use (使用状況を表示)」をクリックし、すべての DOM ノードを表示します。この時点では、開始した直後なので、すべてのノードは赤 (新しい項目) で表示されます。
  4. リークがあるかどうかのテストとして、Web アプリケーション上で何らかのアクションを実行します。
  5. Scan Now (今すぐスキャン)」をクリックし、使用中の DOM ノードの表示を更新します (オプション)。
  6. Show in use (使用状況を表示)」をクリックします。今度は、いくつか興味深い情報がビューに含まれています。孤児ノードが検出される場合や、特定の DOM ノードへの参照が予想外に増加している場合があります。
  7. このレポートを分析し、コードを調べます。
  8. 必要に応じて、ステップ 4 から 8 を繰り返します。

sIEve によってアプリケーションのリークをすべて検出することはできませんが、孤児ノードによって発生するリークは検出することができます。ID や outerHTML などの追加情報により、リークしているノードを特定することができます。リークしているノードを操作しているコードを調べ、必要に応じてコードを変更します。

実際の例

このセクションでは、メモリー・リークを引き起こす状況をさらにいくつか取り上げます。ここで取り上げる例とベスト・プラクティスは Dojo Toolkit を使用していますが、これらの例の大部分は一般的な JavaScript プログラミングにも適用することができます。

クリーンアップを行う場合の一般的なプラクティスは、メモリー・リークを回避するために、DOM を削除して JavaScript オブジェクトを削除します。しかしそれ以外にも必要なことがあります。このセクションのこれから先では、先ほど紹介したパターンを基に説明します。

以下の、皆さんも作成することができるサイトの例では、ページから Web ウィジェットを削除しますが、このアクションは 1 つのページ上で実行され、ページの更新は行われません。リスト 3 は Dojo クラスで定義されたウィジェット (dijit ではありません) を示しています。この記事のこれから先で、このウィジェットを次第に強化していきます。

リスト 3. MyWidget クラス
dojo.declare("leak.sample.MyWidget", null, {
	constructor: function(container) {
this.container = container;
this.ID = dojox.uuid.generateRandomUuid();
this.domNode = dojo.create("DIV", {id: this.ID,
			innerHTML: "MyWidget "+this.ID}, this.container);
	},
	destroy: function() {
this.container.removeChild(dojo.byId(this.ID));
	}
});

リスト 4 はウィジェットを操作するメイン・ページを示しています。

リスト 4. サイトの HTML
<html>
<head>
<title>Dojo Memory Leak Sample</title>
<script type="text/javascript" src="js/dojo/dojo/dojo.js"></script>
<script type="text/javascript">
dojo.registerModulePath("leak.sample", "../../leak/sample");
dojo.require("leak.sample.MyWidget");

widgetArray = [];

function createWidget() {
var container = dojo.byId("widgetContainer");
var widget = new leak.sample.MyWidget(container);
	widgetArray.push(widget);
}
function removeWidget() {
var widget = widgetArray.pop();
	widget.destroy();
}
</script>
</head>
<body>
	<button onclick="createWidget()">Create Widget</button>
	<button onclick="removeWidget()">Remove Widget</button>
	<div id="widgetContainer"></div>
</body>
</html>

dojo.destroy() または dojo.empty() を使用する

一見、たいした問題はないように見えます。ウィジェットが作成され、配列に保存されています。これらのウィジェットはこの配列から取得されて、削除されます。DOM ノードも文書から削除されます。しかし sIEve を使用して create widget アクションと remove widget アクションの違いを追跡すると、ウィジェット・ノードが孤児ノードになるたびにメモリー・リークが発生していることがわかります。図 4 はウィジェットの作成および削除を 2 度行う例を示しています。

図 4. ウィジェット・ノードによるリーク
ウィジェット・ノードによるリーク
ウィジェット・ノードによるリーク

こうした状況が発生しているのは IE のバグのせいかもしれません。ある要素を作成して文書に追加し、それから、その要素を parentNode.removeChild() によって即座に削除しても、相変わらず孤児ノードは存在します。

DOM ノードを削除するには、dojo.destroy() または dojo.empty() を使用することができます。Dojo には、削除されたノードを別の場所に移動した後に破棄する dojo.destroy(<domNode>) が実装されています。dojo.destroy(<domNode>) によって、そうした類のガーベッジ・コレクションのためのノードが作成されます。削除対象であったノードは削除されます (実装の詳細については Dojo のソース・コードを参照してください)。リスト 5 は、この問題を解決するための方法を示しています。

リスト 5. dojo.destroy() を使用して DOM ノードを削除する
## change the destroy() method of MyWidget.js
destroy: function() {
	dojo.destroy(dojo.byId(this.ID));
}

sIEve を使って検証すると、初めてウィジェットを削除する際に Dojo によって空の DIV (ガーベッジ) が作成されることがわかります。その後の追加や削除では、どの DOM ノードも孤児にならないため、リークは発生しません。

JavaScript による DOM ノードへの参照をヌルにする

クリーンアップを行う際には、JavaScript による DOM ノードへの参照をヌルにすることが適切なプラクティスです。リスト 3 では、destroy メソッドは JavaScript による DOM ノード (this.domNode, this.container) への参照をヌルにしていません。ほとんどの場合、このためにメモリー・リークが発生することはありませんが、もっと複雑なアプリケーションで他のオブジェクトが皆さんのウィジェットを参照しているような場合には、問題が発生する可能性があります。

ここで例えば、皆さんの知らない別のリポジトリーがあり、皆さんのウィジェットを参照しているとします。また、何らかの理由から、そのリポジトリーを削除できないとします。そのウィジェットを削除すると、そのリポジトリーが参照していた DOM ノードは孤児になります。この変更をリスト 6 に示します。

リスト 6. サイトの HTML: オブジェクト (widgetRepo) を追加し、ウィジェットを保持する
widgetArray = [];
widgetRepo = {};

function createWidget() {
var container = dojo.byId("widgetContainer");
var widget = new leak.sample.MyWidget(container);
	widgetArray.push(widget);
	widgetRepo[widget.ID] = widget;
}

ここで、ウィジェットの追加および削除を行った後、sIEve を使ってメモリー・リークを検出します。図 5 はウィジェットの DIV に対する孤児ノードを示しており、また widgetContainer DIV への参照が増加していることを示しています。文書の中で widgetContainer DIV への参照として Refs 列に表示される数は 1 でなければなりません。

図 5. 孤児ノード
孤児ノード
孤児ノード

この問題を解決するためには、クリーンアップの際に DOM ノードへの参照をヌルにします (リスト 7)。可能な場合にこれらの null を指定する文を追加することは適切なプラクティスとみなされています。それによって元の関数に影響を与えることはないからです。

リスト 7. DOM への参照をヌルにする
## the destroy method of MyWidget class
destroy: function() {
	dojo.destroy(dojo.byId(this.ID));
this.domNode = null;
this.container = null;
}

イベントとの接続を解除し、トピックをアンサブスクライブする

Dojo では、メモリー・リークを回避するための適切なプラクティスとして、接続したイベントとの接続を解除し、サブスクライブしたトピックをアンサブスクライブする方法があります。リスト 8 はイベントの接続と接続解除の例を示しています。

JavaScript のプログラミングでは通常、DOM ノードに対するイベントを接続解除してから DOM ノードを文書から削除するように推奨されています。ブラウザーの種類に応じて、以下の API を使用してイベントの接続および接続解除を行います。

  • IE の場合: attachEventdetachEvent
  • 他のブラウザーの場合: addEventListenerremoveEventListener
リスト 8. dojo.connect と dojo.disconnect
## the constructor method of MyWidget class
constructor: function(container) {
	// … old code here
this.clickHandler = dojo.connect(
this.domNode, "click", this, "onNodeClick");
}

## the destroy method of MyWidget class
destroy: function() {
	// … old code here
	dojo.disconnect(this.clickHandler);
}

また、トピックのサブスクライブとパブリッシュによって Dojo のコンポーネント間に接続を設定することもできます。これは Observer パターンとして実装されています。この場合のベスト・プラクティスとしては、クリーンアップをする際にはトピックをアンサブスクライブするようにし、メモリー・リークを回避します。サブスクライブとアンサブスクライブのメソッドに対し、以下の API を使用します。

  • dojo.subscribe(/*string*/topic, /*function*/function)
  • dojo.unsubscribe(/*string*/topic)

innerHTML を設定する

JavaScript で innerHTML を設定する場合、注意しないと IE のメモリー・リークが発生する場合があります (詳細は「参考文献」を参照)。リスト 9 は IE のメモリー・リークが発生する状況を示しています。

リスト 9. innerHTML による IE でのリーク
// 1. An orphan node should be in the document
var elem = document.createElement(“DIV”);

// 2. Set the node’s innerHTML with an DOM 0 event wired
elem.innerHTML = “<a onclick=’alert(1)’>leak</a>”;

// 3. Attach the orphan node to the document
document.body.appendChild(elem);

上記のようなコードは Web 2.0 アプリケーションでは一般的なので、注意する必要があります。この問題の解決策としては、ノードが孤児でないことを確認してから innerHTMLを設定します。リスト 9 のコードを修正したものがリスト 10 です。

リスト 10. innerHTML によるリークを修正する
var elem = document.createElement(“DIV”);

// now the node is not orphan anymore
document.body.appendChild(elem);

elem.innerHTML = “<a onclick=’alert(1)’>no leak</a>”;

まとめ

ブラウザーのメモリー・リークを引き起こすパターンは比較的容易に特定することができます。この問題の原因をアプリケーションのソース・コードの中で見つけようとするのは、少し難しい場合があります。sIEve を使うことで、孤児ノードによって発生するリークの大部分を見つけることができます。この記事では、JavaScript に関するちょっとした不注意によってメモリー・リークが発生することを説明しました。この記事で概説したベスト・プラクティスによって、メモリー・リークの発生を防ぐことができます。


ダウンロード可能なリソース


関連トピック

  • Internet Explorer リーク パターンを理解して解決する」(MSDN、2005年6月) を読み、IE のメモリー・リーク・パターンを特定する方法を学んでください。
  • 英語で公開されている「Memory leak patterns in JavaScript」(developerWorks、2007年4月) では、JavaScript の循環参照の基本を説明し、また特にクロージャーと組み合わされた場合、なぜ循環参照によって特定のブラウザーで問題が発生するのかを説明しています。
  • Memory Leakage in Internet Explorer - revisited」(The Code Project、2005年11月) は、少し異なる視点から IE のメモリー・リーク・パターンを解説しています。
  • sIEve について学んでください。
  • IE での .innerHTML によるリークと解決策についての資料を読んでください。
  • developerWorks の Web development ゾーンには Web ベースのさまざまなソリューションを解説した記事が豊富に用意されています。
  • developerWorks podcasts では、ソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
  • Internet Explorer のためのメモリー・リーク検出ツール、sIEve をダウンロードしてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=651218
ArticleTitle=JavaScript と Dojo によってブラウザーで発生するメモリー・リークを発見し、解決する
publish-date=04052011