印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
MSDN
MSDN ライブラリ
テクニカルドキュメント
Internet Explorer 開発
Internet Explorer (全般)
 Internet Explorer リーク パターンを理解して解決する

  低帯域幅での表示をオンにする
Internet Explorer リーク パターンを理解して解決する

Justin Rogers

Microsoft Corporation

June 2005

日本語版最終更新日 2006 年 2 月 3 日

Web 開発者の進化

以前は、メモリ リークは Web 開発者にとって大きな問題ではありませんでした。ページは比較的単純に保たれ、サイト内の異なるロケーション間のナビゲーションは解放されたメモリをクリーンアップするうえで優れた方法でした。リークがあった場合も、たいていは気付かないほど小さなものでした。

新しい Web アプリケーションは、より高い標準に従います。ページはナビゲートされずに何時間も実行され、Web サービスを通じて更新情報を動的に取得する場合があります。複合イベント スキーム、オブジェクト指向の JScript、およびアプリケーション全体を生成するためのクロージャを組み合わせることで、言語機能が限界点に達します。これらの変更およびその他の変更により、特定のメモリ リーク パターンが顕著になります。以前はナビゲーションによって隠されていたメモリ リーク パターンは特に顕著になります。

さいわい、メモリ リーク パターンは、探す対象がわかっている場合には簡単に特定できます。発生する可能性のある厄介なパターンのほとんどには既知の対処方法があり、自分で行う必要のある追加作業はわずかです。それでも一部のページに小さなメモリ リークが発生することがありますが、最も目立つメモリ リークは簡単に除去できます。

リーク パターン

次の各節では、メモリ リークのパターンについて説明し、各パターンの一般的な例を示します。1 つの大きなパターン例は JScript のクロージャ機能であり、もう一つの例はフック イベントでのクロージャの使用です。イベント フックの例に精通している場合はメモリ リークの多くを見つけて修正できる可能性がありますが、その他のクロージャ関連の問題には気付かない場合があります。

ここで、次のパターンを見てみましょう。

  1. 循環参照 - Internet Explorer の COM インフラストラクチャと任意のスクリプティング エンジンの間で相互参照がカウントされている場合は、オブジェクトによってメモリ リークが発生することがあります。これは最も明白なパターンです。

  2. クロージャ - クロージャは、既存の Web アプリケーション アーキテクチャに対して最大のパターンをもたらす特殊な形式の循環参照です。クロージャは、特定の言語キーワードに依存しており、包括的に検索できるため、簡単に特定できます。

  3. クロスページ リーク - クロスページ リークは、サイトからサイトに移動するときに発生する内部的なブックキーピング オブジェクトのリークで、通常は非常に小さいものです。DOM 挿入順序の問題に加えて、コードにわずかな変更を加えることでこれらのブックキーピング オブジェクトの作成を防ぐ方法を示す対処方法を検証します。

  4. 擬似リーク - これらは実際にはリークではありませんが、メモリの経過を理解していない場合には非常に面倒な場合があります。スクリプト要素のリライトと、そのリライトが必要に応じて実際に実行される際にごくわずかなメモリがリークしていくようすについて検証します。

循環参照

循環参照は、ほぼすべてのリークの根本原因です。通常、スクリプト エンジンはガベージ コレクションを通じて循環参照を処理しますが、特定の未知項目によってヒューリスティックスの正常な動作が妨害されることがあります。IE のケースでの未知項目は、スクリプトの一部分がアクセス権を持つ DOM 要素のステータスです。基本的な原理は次のとおりです。

図

1. 基本的な循環参照パターン

このパターンのリークの原因は、COM 参照カウントに基づいています。スクリプト エンジン オブジェクトは、DOM 要素への参照を保持し、DOM 要素ポインタのクリーンアップと解放を行う前に未解決の参照が削除されるのを待ちます。この例では、スクリプト エンジン スコープと、DOM 要素の expando プロパティという、スクリプト エンジン オブジェクトに対する 2 つの参照があります。スクリプト エンジンを終了すると最初の参照は解放されますが、DOM 要素参照はスクリプト エンジン オブジェクト上で解放されるのを待っているため解放されません。このシナリオを検出して問題を修正するのは簡単なように思えるかもしれませんが、実際には、ここで示した基本的なケースは氷山の一角にすぎません。30 個のオブジェクト チェーンの最後に循環参照があることもあり、このような循環参照の検出ははるかに困難になります。

このパターンが HTML でどのように見えるかを知るために、次に示すようなグローバル スクリプト エンジン変数と DOM 要素を使用してリークを発生させることができます。

<html>
<head>
<script language="JScript">
var myGlobalObject;
function SetupLeak()
        {
// 最初にスクリプト スコープから要素への参照を設定します。
myGlobalObject =
document.getElementById("LeakedDiv");
// 次に、要素からスクリプト スコープへの参照を設定します。
document.getElementById("LeakedDiv").expandoProperty =
myGlobalObject;
        }
function BreakLeak()
        {
document.getElementById("LeakedDiv").expandoProperty =
null;
        }
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>

リーク パターンを解決するために、明示的な null 割り当てを利用できます。ドキュメントをアンロードする前に null を割り当てることにより、スクリプト エンジンに対して、エンジン内の要素とオブジェクト間の関連がなくなったことを通知します。ここで、参照を適切にクリーンアップし、DOM 要素を解放できます。このケースでは、スクリプト エンジンよりも Web 開発者の方がオブジェクト間の関係について詳しく知っています。

これは基本的なパターンですが、より複雑なシナリオを突き止めるのは困難な場合があります。オブジェクト指向 JScript の一般的な使用方法では、JScript オブジェクトの内部に DOM 要素をカプセル化することにより、DOM 要素を拡張します。構築プロセス中に、通常はアタッチ先の DOM 要素を渡し、新規に作成したオブジェクトのインスタンスを DOM 要素に格納する一方で、新規に作成したオブジェクトに DOM 要素への参照を格納します。これにより、アプリケーション モデルでは必要なすべてのものにいつでもアクセスできます。これは非常に明示的な循環参照ですが、さまざまな言語機能を使用するため、この循環参照に気付かない場合があることが問題になります。この種のパターンの解決はより複雑になる場合がありますが、前述の単純な方法を活用できます。

<html>
<head>
<script language="JScript">
function Encapsulator(element)
        {
// 要素を設定します。
this.elementReference = element;
// 循環参照を作成します。
element.expandoProperty = this;
        }
function SetupLeak()
        {
// リークがすべて一度に発生します。
new Encapsulator(document.getElementById("LeakedDiv"));
        }
function BreakLeak()
        {
document.getElementById("LeakedDiv").expandoProperty =
null;
        }
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>

この問題に対するより複雑な解決方法では、登録スキームを使用してアンフックする必要のある要素/プロパティを示し、ドキュメントをアンロードする前にピア要素をクリーンアップできるようにピア要素のフック イベントを使用しますが、問題を実際に修正しないと追加のリーク パターンが発生することがよくあります。

クロージャ

クロージャは、プログラマにまったく認識させずに循環参照を作成するため、リークを発生させることが非常に多くあります。親関数パラメータとローカル変数が適切なタイミングで固定され、参照され、クロージャ自体が解放されるまで保持されることはすぐには明らかになりません。実際に、これは一般的なプログラミング戦略になっており、ユーザーの間では使用可能なリソースが非常に少なくなっているという問題がよく発生しています。クロージャの背後にある歴史的要素とクロージャ リークに関するいくつかの具体例がユーザーによって詳細に述べているため、クロージャ モデルを循環参照ダイアグラムに適用し、これらの追加参照がどこから生じているかを調べた後で、それらについて確認します。

図

2. クロージャでの循環参照

通常の循環参照では、相互への参照を保持する 2 つのソリッド オブジェクトがありましたが、クロージャは異なります。直接参照する代わりに、親関数のスコープから情報をインポートすることで参照が行われます。通常、関数のローカル変数と、関数を呼び出すときに使用されるパラメータは、関数自体の継続期間中のみ存在します。クロージャを使用すると、これらの変数とパラメータは、クロージャが有効である限り未解決の参照を持ち続けます。また、クロージャは親関数の継続期間後も存在できるため、その関数のローカル変数とパラメータもすべて存在できます。例では、通常、パラメータ 1 は関数呼び出しが終了するとすぐに解放されます。クロージャを追加したため、第 2 の参照が作成されます。この第 2 の参照は、クロージャが解放されるまで解放されません。クロージャをイベントにアタッチした場合は、そのイベントからデタッチする必要があります。クロージャを expando にアタッチした場合は、その expando を null にする必要があります。

クロージャは呼び出しごとにも作成されるため、この関数を 2 回呼び出すと 2 つの異なるクロージャが作成され、各クロージャは毎回渡されたパラメータへの参照を保持します。この透過的な性質により、クロージャのリークは実に簡単に発生します。次の例で、クロージャを使用した最も基本的なリークを示します。

<html>
<head>
<script language="JScript">
function AttachEvents(element)
        {
// この構造体により、要素が ClickEventHandler を参照します。
element.attachEvent("onclick", ClickEventHandler);
function ClickEventHandler()
            {
// このクロージャは要素を参照します。
            }
        }
function SetupLeak()
        {
// リークがすべて一度に発生します。
AttachEvents(document.getElementById("LeakedDiv"));
        }
function BreakLeak()
        {
        }
</script>
</head\>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>

このリークの解決方法を知るのは、通常の循環参照ほど簡単ではありません。"クロージャ" は、関数スコープ内に存在する一時オブジェクトと見なすことができます。関数を終了すると、クロージャ自体への参照が失われますが、最後の detachEvent 呼び出しで何を行いますか。この問題に対する最も興味深いアプローチの 1 つは、「MSN spaces thanks to Scott Isaacs」 (英語) で示されています。このアプローチでは、第 2 のクロージャを使用して、ウィンドウの onUnload イベントを追加でフックします。このクロージャには同じ "スコープ化された" オブジェクトがあるため、イベントのデタッチ、自身のデタッチ、およびクリーンアップ プロセスの完了が可能です。すべてをこのモデルに簡単に適合させるために、次の例に示すように、クロージャを expando に格納し、デタッチしてから、expando を null にすることもできます。

<html>
<head>
<script language="JScript">
function AttachEvents(element)
        {
// これを削除するために、
// どこかに置く必要があります。別の参照を作成します。
element.expandoClick = ClickEventHandler;
// この構造体により、要素が ClickEventHandler を参照します。
element.attachEvent("onclick", element.expandoClick);
function ClickEventHandler()
            {
// このクロージャは要素を参照します。
            }
        }
function SetupLeak()
        {
// リークがすべて一度に発生します。
AttachEvents(document.getElementById("LeakedDiv"));
        }
function BreakLeak()
        {
document.getElementById("LeakedDiv").detachEvent("onclick",
document.getElementById("LeakedDiv").expandoClick);
document.getElementById("LeakedDiv").expandoClick = null;
        }
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>

実際には、サポート技術情報の記事 (英語) では、必要でない限りクロージャを使用しないことをお勧めしています。例では、クロージャをイベント ハンドラとして使用する必要はなく、グローバル スコープへクロージャを移動できることを示しました。クロージャが関数になると、パラメータまたはローカル変数を親関数から継承しなくなるため、クロージャベースの循環参照を気にする必要はまったくなくなります。必要でなければクロージャに依存しないアーキテクチャを作成することにより、コードの大部分を修正できます。

最後に、スクリプティング エンジンの開発者の 1 人である Eric Lippert によるクロージャ全般に関するすばらしい記事 (英語) があります。彼の最終的な推奨も、本当に必要な場合にのみクロージャを使用するということで一致しています。彼の記事ではクロージャ パターンの対処方法については言及していませんが、実際に作業を開始するために十分な例をここで示したつもりです。

クロスページ リーク

挿入の順序に基づくリークのほとんどは、適切にクリーンアップされない中間オブジェクトの作成が原因です。厳密には、動的要素を作成してから DOM にアタッチするケースです。動的に作成された 2 つのオブジェクトどうしを一時的にアタッチして子要素から親要素へのスコープを作成するのが基本的なパターンです。後でこの 2 要素のツリーをプライマリ ツリーにアタッチすると、その両方がドキュメントのスコープを継承し、一時オブジェクトがリークします。次の図に、動的に作成された要素をツリーにアタッチするための 2 つの方法を示します。最初のモデルでは、各子要素を親にアタッチし、最終的にサブツリー全体をプライマリ ツリーにアタッチします。この方法では、他の条件が満たされる場合に一時オブジェクトでリークが発生することがあります。2 つ目のモデルでは、要素をプライマリ ツリーにアタッチして、動的に作成された最上位レベル要素からすべての子まで進みます。各アタッチではプライマリ ドキュメントのスコープが継承されるため、一時スコープが生成されることはありません。この方法は、潜在的なメモリ リークの回避においてはるかに優れています。

図

3. DOM 挿入順序リーク   モデル

次に、ほとんどのリーク検出アルゴリズムに対して透過的なリークの例を示します。パブリックに参照可能な要素はリークせず、リークするオブジェクトは非常に小さいため、この問題に気付かない場合があります。例を機能させるために、動的に作成された要素には、インライン関数の形式のスクリプト ポインタを含める必要があります。これにより、要素どうしをアタッチするときに一時的に作成された内部スクリプト オブジェクトがリークすることがあります。リークは小さいため、数千のサンプルを実行する必要があります。実際に、リークするオブジェクトはわずか数バイトです。サンプルを実行し、空のページにナビゲートすることにより、2 つのバージョンのメモリ消費の差異を確認できます。子を親にアタッチしてから親をプライマリ ツリーにアタッチする最初の DOM モデルを使用した場合は、メモリ使用量が若干増えます。これはナビゲーション間リークであり、IE プロセスが再開するまでメモリは再生されません。親をプライマリ ツリーにアタッチしてから子を親にアタッチする第 2 の DOM モデルを使用してサンプルをさらに数回実行した場合、メモリの増加は継続せず、クロスページ ナビゲーション リークが解決したことがわかります。

<html>
<head>
<script language="JScript">
function LeakMemory()
        {
var hostElement = document.getElementById("hostElement");
// 何度も実行して、タスク マネージャでメモリの反応を確認します。
for(i = 0; i < 5000; i++)
            {
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");
// これにより一時オブジェクトがリークします。
parentDiv.appendChild(childDiv);
hostElement.appendChild(parentDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
            }
hostElement = null;
        }
function CleanMemory()
        {
var hostElement = document.getElementById("hostElement");
// 何度も実行して、タスク マネージャでメモリの反応を確認します。
for(i = 0; i < 5000; i++)
            {
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");
// 順序の変更は重要で、これによりリークしなくなります。
hostElement.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
            }
hostElement = null;
        }
</script>
</head>
<body>
<button onclick="LeakMemory()">Memory Leaking Insert</button>
<button onclick="CleanMemory()">Clean Insert</button>
<div id="hostElement"></div>
</body>
</html>

ここで使用する対処方法は IE のベスト プラクティスに対するものであるため、このリークについて説明しておく価値があります。リークについて理解する必要のある重要なポイントは、スクリプトがアタッチされた状態で DOM 要素が作成されることです。スクリプトを含まない DOM 要素を作成し、同じ方法でこれらをアタッチした場合はリークの問題が生じないため、スクリプトのアタッチはリークに対して不可欠です。このことから、より大きなサブツリーに対してより適している可能性のある第 2 の対処方法が生み出されます (例では 2 つの要素しか使用していないため、プライマリ DOM からツリーを構築してもパフォーマンスに影響しません)。第 2 の対処方法では、当初はスクリプトがアタッチされていない要素を作成して、サブツリーを安全に構築できるようにします。サブツリーをプライマリ DOM にアタッチした後で、そのポイントに戻ってスクリプト イベントを設定します。循環参照とクロージャの原理に従い、イベントをフックする際にコードに別のリークが発生しないようにしてください。

すべてのメモリ リークが簡単に見つかるわけではないことを示すために、この問題を指摘しておく必要がありました。小さなパターンが目に見えるようになるには数千回の反復が必要であり、そのパターンは、問題を発生させる DOM 要素の挿入順序などの些細なものである場合があります。ベスト プラクティスのみ使用してプログラミングを行えば安全であると考えがちですが、このリークはベスト プラクティスでもリークが発生することを示しています。ここで示した解決策では、リーク条件を除去するために、ベスト プラクティスを改善し、新しいベスト プラクティスも導入しました。

擬似リーク

一部の API の実際の動作と予想される動作との違いから、メモリ リークを誤って診断してしまうことがよくあります。擬似リークは、ほぼ常に動的スクリプティング操作中に同じページに出現し、ページからブランク ページへのナビゲーション後は目に見えることがまれです。このため、問題をクロスページ リークとして排除してから、メモリ消費が予想されるかどうかについての作業を開始できます。擬似リークの例として、スクリプト テキストのリライトを使用します。

DOM 挿入順序の問題と同様に、この問題でも、メモリを "リーク" させるために一時オブジェクトを作成します。スクリプト要素の内部のスクリプト テキストを何度もリライトすることにより、前のコンテンツにアタッチされていたさまざまなスクリプト エンジン オブジェクトが徐々にリークし始めます。特に、デバッグ スクリプトに関連するオブジェクトは、完全に形式化されたコード要素であるため取り残されます。

<html>
<head>
<script language="JScript">
function LeakMemory()
        {
// 何度も実行して、タスク マネージャでメモリの反応を確認します。
for(i = 0; i < 5000; i++)
            {
hostElement.text = "function foo() { }";
            }
        }
</script>
</head>
<body>
<button onclick="LeakMemory()">Memory Leaking Insert</button>
<script id="hostElement">function foo() { }</script>
</body>
</html>

上記のコードを実行し、タスク マネージャの手段を再び使用した場合、"リークしている" ページとブランク ページ間のナビゲート中はスクリプト リークに気付きません。このスクリプト リークは完全にページ内で発生し、ページからナビゲートするとメモリが戻されます。この欠陥が生じるのは、予期した動作と実際の動作が異なるためです。ユーザーは一部のスクリプトをリライトした後に元のスクリプトが残らないものと想定しています。しかし実際には、イベントのアタッチに既に使用され、未解決の参照カウントがある可能性があるため、元のスクリプトが残ります。おわかりのように、これは擬似リークです。表面上はメモリ消費量が非常に悪いように見えますが、まったく正当な理由があります。

まとめ

すべての Web 開発者は、リークがあるとわかっているコード例のリストを個人的に作成し、コード内にその例が見られた場合はリークに対処する必要があることを認識しています。これは非常に便利であり、現在 Web にリークが比較的少ない理由でもあります。個々のコード例ではなくパターンとしてリークについて考えると、リークを処理するためのより優れた戦略の開発を始めることができます。アイデアとして、デザイン フェーズ中にリーク パターンを考慮し、潜在的なリークに対するプランがあることを確認します。防衛的なコーディング手法を使用し、自身のメモリをすべてクリーンアップする必要があることを前提としてください。これは大げさで、自身のメモリをクリーンアップする必要があるのは非常にまれですが、クリーンアップすることで、どの変数と expando プロパティにリークの可能性があるかが明らかになります。

パターンとデザインに興味がある場合は、「Scott's short blog entry」 (英語) を強くお勧めします。このドキュメントは、すべてのクロージャベース リークを取り除く汎用的な例を示しています。必要なコードは若干増えますが、手法は健全であり、改良されたパターンはコード内での特定とデバッグが簡単です。登録方法自体にリークの問題がないことに注意していれば、(特にクロージャが使用されている場合に) 類似の登録スキームを expando ベースの循環参照に使用できます。

執筆者紹介  

Justin Rogers は、最近、拡張性について作業するオブジェクト モデル開発者として Internet Explorer チームに参加しました。以前は、.NET QuickStart Tutorial、.NET Terrarium、SQL Server 2005 の SQL Reporting Services Management Studio などの注目されるプロジェクトについて作業していました。

© 2009 Microsoft Corporation. All rights reserved. 使用条件  |  商標  |  プライバシー
Page view tracker