メモリリークの特定

「メモリリーク」は様々な意味合いで使用されます。メモリ使用量を意味することがあります。Wikipedia(英語版)では、「メモリリーク」を以下のように定義しています。

「コンピュータプログラムがメモリの割り当てを正しく管理できないこと。」

これは妥当な定義ですが、若干あいまいです。

本ガイドでの定義

本ガイドでは、メモリリークを以下のように定義します。

コードの一部を繰り返した後に、メモリ使用が制限なく拡大していること。コードは(メモリを解放するのに必要十分な程度まで)「最後まで」繰り返す必要があり、さらにコードは妥当な言語 / フレームワークのクリーンアップが確実に実行されるようにする必要があります。

これはちょっと長い説明ですので、この定義の重要なポイントを分けて見てみましょう。

言語/フレームワークのクリーンアップ

プログラムの実行環境によって、割り当てメモリの特定部分を完了したことを示す操作に関するルールが通常あります。Ext JS では、通常これは destroy メソッドです。このメソッドは通常 DOM 要素をクリーンアップしてリスナをアンバインドします。

C# では、推奨パターンは IDisposable インターフェイスです。プラットフォームに関係なく、プラットフォームが割り当てられたリソースを解放できるようにこれらの表記規則に従う必要があります。クリーンアップ手順に従わない場合、リソースが必要なくなった場合に自動的に推測できないので、メモリリークが発生します。

最後まで繰り返す

空きメモリ容量が64 GB の開発マシンがあるとします。コードのセクションは5回実行されます。調べてみると、実行ごとにメモリ使用量が毎回1 MB 増え、解放されません。

これは特に問題とはなりません。プログラムはわずかなメモリしか使用しません。コードセクションが50,000回繰り返されメモリが解放されていないと、結果は違います。基礎となるシステムは強制的にメモリを解放するほどまで逼迫している必要があります。

使用量が無限に拡大

これはおそらく最も微妙ですが、定義の最重要部分です。多くの場合、destroy やその他のクリーンアップを呼び出すと割り当てリソースすべては解放されません。Ext JS では、これはキャッシュに見られます。

例えば、文字列セレクタに基づくコンポーネントを検索するために Ext.ComponentQuery クラスが使用されます。内部的に、この文字列セレクタは関数に変換され、候補となるコンポーネントで実行できます。この関数の構築には負荷がかかり、多くの場合に同じクエリを複数回実行します。この再利用のために、生成される関数はメモリに維持されます。ここで重要なのはキャッシュメカニズムが制限される点です。

キャッシュは LRU(最も長く使われていない)キャッシュです。LRU はコレクションのアイテムへのアクセスを追跡します。アイテムにアクセスがあると、前面に引き出されます。また、LRU キャッシュには最大サイズがあります。アイテムが最大サイズを超えて追加されると、最も長く使用されていないアイテムがキャッシュから追放されます。最大制限に達すると、キャッシュは正規化されます。このようなものがメモリ内に残っても問題にはなりません。制限なしにリソースが保持される場合のみ問題となります。

抽象化とガベージコレクション

Ext JS を使用する開発者は実際のメモリ管理とは縁がありません。さらにやっかいなことに、Window タスク マネージャーや Mac アクティビティモニタなどのツールはメモリ消費を正確に示しません。原因と結果の関係がどれほど乖離しているかを理解するために、メモリ管理の階層を評価することは重要です。

割り当て

  • 開発者はフレームワークからのリソースをリクエストします(コンポーネントの作成など)。
  • フレームワークは JavaScript エンジンからのリソースをリクエストします(多くの場合に new または createElement などの演算子を使用)。
  • JavaScript エンジンは基になるプロセスメモリマネージャーからのリソースをリクエストします(通常、C++ メモリ割り当て)。
  • 基になるメモリマネージャーはオペレーティングシステムからのリソースをリクエストします。これはタスク マネージャーやアクティビティモニタで見られるメモリ使用量です。

クリーンアップ

  1. 開発者は Ext JS コンポーネントまたはその他のリソースで destroy を呼び出します。
  2. Ext JS コンポーネントの destroy メソッドは他のクリーンアップメソッドを呼び出し、様々な内部参照を null などに設定します。
  3. JavaScript ガベージコレクタはヒープに対してスイープを行い、メモリを解放するタイミングを後で決定します。多くの場合、これは新しいメモリがリクエストされ、空きメモリが「不十分」になるまで遅らせます。特にアプリケーションの初期段階ではヒープを拡大するほうがコストをかけずにできるので、メモリマネージャーはガベージコレクションを行う代わりに再度ヒープを拡大させるか決定するだけです。
  4. JavaScript メモリマネージャーがガベージを収集することを決定すると、解放されるメモリを今後使用するための空きメモリとして保持するか、基になるプロセスヒープに戻すかを決定する必要があります。
  5. JavaScript メモリマネージャー(通常、C++ メモリマネージャー)が使用する基になるメモリマネージャーによっては、今後そのプロセスで使用するための空きメモリが保持されるか、オペレーティングシステムに戻されます。この時点になって初めてタスク マネージャー/アクティビティモニタで更新されます。

上記から、JavaScript 開発者がメモリ管理に関してコントロールできることは限られていることが分かります。多くの可動部分があり、実際のメモリ管理は非常に小さな歯車です。

本ガイドでは、これらの階層については詳しく説明しません。JavaScript ヒープとガベージコレクタが適切と思われる操作を実行し、特定の動作になるように強制できないと言えます。これを行うためには、リファレンスがユーザーコードまたはフレームワークによって保持されないようにすることが一番です。

結局は、一般的な OS の監視ツールでメモリ使用量を調べて使用量を監視しても、「メモリリーク」を必ずしも示すわけではありません。

リークの検出

アプリケーションレベルのリーク

アプリケーションがフレームワークのリソースのクリーンアップに失敗すると、オブジェクトはフレームワークが維持するコレクションに蓄積するようになります。これらの正確な詳細はバージョン固有ですが、以下などを参照できます。

フレームワークレベルのリーク

フレームワーク内部のリソースをクリーンアップするように全力を尽くしても、エラーの可能性は常にあります。今まで、最も一般的な問題は DOM 要素のリークによるものです。このようなケースが考えられる場合、sIEve ツールが Internet Explorer で優れたリーク検出を提供します。

注意:このような低レベルで何かを見つける前に、すべてのアプリケーションレベルのリークをすべて解決することを強く推奨します。

一般的なコードリークパターンとソリューション

以下のコードスニペットと記述は、問題となるような様々なメモリの乱用を示しています。

ベースクラスクリーンアップの回避

派生クラスのリソースをクリーンアップしようとして、ベースクラスクリーンアップが誤ってバイパスされることがあります。

例:

Ext.define('Foo.bar.CustomButton', {
    extend: 'Ext.button.Button',
    onDestroy: function () {
        // do some cleanup
    }
});

ソリューション:ベースクラスがそのクリーンアップを実行できるようにl callParent() を呼び出してください。

DOM リスナを削除しない

イベントは要素に添付されます。要素は innerHTML を変更することによって上書きされます。ただし、このイベントハンドラはメモリに残ります。

Ext.fly(someElement).on('click', doSomething);

someElement.parentNode.innerHTML = '';

ソリューション:重要な要素への参照を維持し、必要なくなったら destroy メソッドを呼び出します。

オブジェクトへの参照を維持

メモリ使用量の多いクラスのインスタンスが作成されます。クラスは破棄されますが、参照は既存のオブジェクトに残ります。

Ext.define('MyClass', {

    constructor: function() {
        this.foo = new SomeLargeObject();
    },

    destroy: function() {
        this.foo.destroy();
    }
});

this.o = new MyClass();
o.destroy();

// `this` still has a reference to `o` and `o` has a reference to `foo`.

ソリューション:参照を null に設定して、メモリを解放できるようにします。この場合、destroy で this.foo = null、destroy 呼び出し後に this.o = null です。

クロージャで参照を維持

このソリューションはさらに細かくなりますが、上記に非常に似ています。クロージャは引き続き参照しながら、解放できない大規模オブジェクトへの参照を保持します。

function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;

    // other things

    return function() {
        return x;  // o is in closure scope but not needed
    }
}

var f = runAsync(1);

大規模オブジェクトが外部スコープにあり内部関数を必要としないため、多くの場合に上記が発生します。このようなことは見逃しやすいですが、メモリ使用量に悪影響を与えます。

ソリューション:Ext.Function.bind() または標準 JavaScript 関数 bind を使用して、このような関数の外部で宣言された関数の安全なクロージャを作成します。

function fn (x) {
    return x;
}

function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;

    // other things

    return Ext.Function.bind(fn, null, [x]); // o is not captured
}

var f = runAsync(1);

副次的影響のあるインスタンスを継続的に作成

一部のオブジェクトの作成には副次的影響があります(DOM 要素の作成など)。破棄されずに作成されると、メモリのリークにつながります。

{
    xtype: 'treepanel',
    listeners: {
        itemclick: function(view, record, item, index, e) {

            // Always creating and rendering a new menu
            new Ext.menu.Menu({
                items: [record.get('name')]
            }).showAt(e.getXY());
        }
    }
}

ソリューション:メニューへの参照を取得し、必要なくなったら destroy メソッドを呼び出します。

キャッシュで登録をクリア

オブジェクトへのすべての参照を削除することが重要です。ローカル参照を null に設定するだけでは十分ではありません。一部のグローバルシングルトンキャッシュが参照を保持している場合、その参照はアプリケーションのライフサイクル中に保持されます。

var o = new SomeLargeObject();
someCache.register(o);

// Destroy and null the reference. someCache still has a reference
o.destroy();
o = null;

ソリューション:destroy の呼び出しに加えて、追加されたキャッシュからオブジェクトを削除します。

要約

アプリケーションのメモリ管理のコントロールは簡単な作業です。未使用のコンポーネントを破棄し、未使用の参照を無効にして、callParent() を使用して、アプリケーションの優れた状態を維持します。これらの提案に従うことによって、アプリケーションはスムーズに実行し、リソースを無責任に使用しません。

Last updated