ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript プログラムのメモリ リークのグラフィック コードの詳細な紹介

JavaScript プログラムのメモリ リークのグラフィック コードの詳細な紹介

黄舟
黄舟オリジナル
2017-03-09 14:11:011418ブラウズ

JavaScript プログラムのメモリ リークの詳細なグラフィック コードの紹介:

ガベージ コレクションは私たちを解放し、(メモリ管理ではなく) アプリケーション ロジックに集中できるようにします。ただし、ガベージ コレクションは魔法ではありません。その仕組みと、ずっと前に解放されているはずだったメモリを保持する方法を理解することで、アプリケーションの高速化と信頼性の向上につながる可能性があります。この記事では、JavaScript アプリケーションのメモリ リークを特定する体系的なアプローチ、いくつかの一般的なリーク パターン、およびこれらのリークを解決するための適切な方法を学びます。

はじめに

JavaScript のようなスクリプト言語を扱う場合、すべてのオブジェクト、クラス、文字列、数値、メソッドでメモリの割り当てと予約が必要であることを忘れがちです。メモリの割り当てと割り当て解除の具体的な詳細は、言語およびランタイム ガベージ コレクターからは隠されています。

多くの機能はメモリ管理を考慮せずに実装できますが、メモリ管理を無視するとプログラムに重大な問題が発生する可能性があります。不適切にクリーニングされたオブジェクトは、予想よりもはるかに長く存続する可能性があります。これらのオブジェクトは引き続きイベントに応答し、リソースを消費します。これらはブラウザに仮想ディスク ドライブからメモリ ページを強制的に割り当て、コンピュータの速度を大幅に低下させます (極端な場合にはブラウザのクラッシュを引き起こします)。

メモリ リークとは、所有または必要がなくなった後も存続するオブジェクトです。近年、多くのブラウザでは、ページの読み込み中に JavaScript からメモリを再利用する機能が向上しています。ただし、すべてのブラウザが同じように動作するわけではありません。 Firefox と Internet Explorer の古いバージョンの両方で、ブラウザを閉じるまで続くメモリ リークが発生しました。

過去にメモリ リークを引き起こした多くの古典的なパターンは、最新のブラウザではメモリ リークを引き起こさなくなりました。ただし、現在、メモリ リークに影響を与える別の傾向があります。多くの企業は、ハード ページを更新せずに単一ページで実行される Web アプリケーションを設計しています。このような単一ページでは、アプリケーションのある状態から別の状態に移行するときに、不要になったメモリや関連性のなくなったメモリが保持されやすくなります。

この記事では、オブジェクトの基本的なライフサイクル、ガベージ コレクションがオブジェクトが解放されたかどうかを判断する方法、および潜在的なリーク動作を評価する方法について学びます。さらに、Google Chrome のヒープ プロファイラーを使用してメモリの問題を診断する方法を学びます。いくつかの例は、クロージャ、コンソール ログ、ループからのメモリ リークを解決する方法を示しています。

オブジェクトのライフサイクル

メモリ リークを防ぐ方法を理解するには、オブジェクトの基本的なライフサイクルを理解する必要があります。オブジェクトが作成されると、JavaScript はそのオブジェクトに適切なメモリを自動的に割り当てます。この瞬間から、ガベージ コレクターは継続的にオブジェクトを評価して、それがまだ有効なオブジェクトであるかどうかを確認します。

ガベージ コレクターはオブジェクトを定期的にスキャンし、各オブジェクトへの参照を持つ他のオブジェクトの数をカウントします。オブジェクトの参照が 0 である (他のオブジェクトがそれを参照していない) 場合、またはオブジェクトへの参照が循環のみである場合、オブジェクトのメモリを再利用できます。図 1 は、ガベージ コレクターがメモリを再利用する例を示しています。

図 1. ガベージ コレクションによるメモリの再利用

展示与各个对象关联的 root 节点的 4 个步骤。

このシステムが実際に動作しているのを見ると役に立つでしょうが、この機能を提供するツールは限られています。 JavaScript アプリケーションがどれだけのメモリを使用しているかを知る方法の 1 つは、システム ツールを使用してブラウザのメモリ割り当てを確認することです。現在の使用量と、プロセスのメモリ使用量の経時的な傾向グラフを提供できるツールがいくつかあります。

たとえば、Mac OSX に XCode がインストールされている場合は、Instruments アプリを起動し、そのアクティビティ モニター ツールをブラウザに接続して、リアルタイム分析を行うことができます。 Windows® では、タスク マネージャーを使用できます。アプリケーションの使用中に時間の経過とともにメモリ使用量が着実に増加していることに気付いた場合は、メモリ リークが発生していることがわかります。

ブラウザのメモリ使用量を観察することは、JavaScript アプリケーションの実際のメモリ使用量を非常に大まかに示すだけです。ブラウザーのデータからは、どのオブジェクトが漏洩したかがわかりません。また、データが実際にアプリケーションの実際のメモリ フットプリントと一致するという保証もありません。また、一部のブラウザの実装上の問題により、対応する要素がページ内で破棄された場合、DOM 要素 (または代替のアプリケーション レベルのオブジェクト) が解放されない場合があります。これは、ブラウザがより複雑なインフラストラクチャを実装する必要があるビデオ タグに特に当てはまります。

クライアント側の JavaScript ライブラリにメモリ割り当ての追跡を追加する試みが数多く行われてきました。残念ながら、どの試みも特に信頼できるものではありませんでした。たとえば、人気のある stats.js パッケージは不正確なためサポートされていません。一般に、クライアントからこの情報を維持または決定しようとすると、アプリケーションにオーバーヘッドが発生し、確実に終了できないため、問題が発生します。

理想的な解決策は、ブラウザ ベンダーが、メモリ使用量を監視し、リークしたオブジェクトを特定し、特定のオブジェクトがまだ予約済みとしてマークされている理由を判断するのに役立つ一連のツールをブラウザ内に提供することです。

現在、開発者ツールとしてメモリ管理ツールを実装しているのは、Google Chrome (ヒープ プロファイルを提供する) のみです。この記事では、ヒープ プロファイラーを使用して、JavaScript ランタイムがメモリをどのように処理するかをテストし、デモンストレーションします。

ヒープ スナップショットの分析

メモリ リークを作成する前に、メモリを適切に収集する単純な対話を確認してください。まず、リスト 1 に示すように、2 つのボタンを持つ単純な HTML ページを作成します。

リスト 1.index.html

<html>
<head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" 
type="text/javascript"></script>
</head>
<body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
    <script src="assets/scripts/leaker.js" type="text/javascript" 
charset="utf-8"></script>
    <script src="assets/scripts/main.js" type="text/javascript" 
charset="utf-8"></script>
</body>
</html>

jQuery を組み込むことで、さまざまなブラウザー間で機能し、最も一般的な開発慣行に厳密に準拠してイベント バインディングを管理するための単純な構文が保証されます。 leaker クラスとメインの JavaScript メソッドに script タグを追加します。開発環境では、多くの場合、JavaScript ファイルを 1 つのファイルに結合することをお勧めします。この例では、ロジックを別のファイルに配置する方が簡単です。 leaker 类和主要 JavaScript 方法添加脚本标记。在开发环境中,将 JavaScript 文件合并到单个文件中通常是一种更好的做法。出于本示例的用途,将逻辑放在独立的文件中更容易。

您可以过滤 Heap Profiler 来仅显示特殊类的实例。为了利用该功能,创建一个新类来封装泄漏对象的行为,而且这个类很容易在 Heap Profiler 中找到,如清单 2 所示。

清单 2. assets/scripts/leaker.js

var Leaker = function(){};
Leaker.prototype = {
    init:function(){

    }    
};

绑定 Start 按钮以初始化 Leaker 对象,并将它分配给全局命名空间中的一个变量。还需要将 Destroy 按钮绑定到一个应清理 Leaker 对象的方法,并让它为垃圾收集做好准备,如清单 3 所示。

清单 3. assets/scripts/main.js

$("#start_button").click(function(){
    if(leak !== null || leak !== undefined){
        return;
    }
  leak = new Leaker();
  leak.init();
});

$("#destroy_button").click(function(){
    leak = null;
});

var leak = new Leaker();

现在,您已准备好创建一个对象,在内存中查看它,然后释放它。

  1. 在 Chrome 中加载索引页面。因为您是直接从 Google 加载 jQuery,所以需要连接互联网来运行该样例。

  2. 打开开发人员工具,方法是打开 View 菜单并选择 Develop 子菜单。选择 Developer Tools 命令。

  3. 转到 Profiles 选项卡并获取一个堆快照,如图 2 所示。

    图 2. Profiles 选项卡

    Google Chrome 上的 profiles 选项卡的快照。

  4. 将注意力返回到 Web 上,选择 Start

  5. 获取另一个堆快照。

  6. 过滤第一个快照,查找 Leaker 类的实例,找不到任何实例。切换到第二个快照,您应该能找到一个实例,如图 3 所示。

    图 3. 快照实例

    Heap Profiler 过滤器页面的快照

  7. 将注意力返回到 Web 上,选择 Destroy

  8. 获取第三个堆快照。

  9. 过滤第三个快照,查找 Leaker 类的实例,找不到任何实例。在加载第三个快照时,也可将分析模式从 Summary 切换到 Comparison,并对比第三个和第二个快照。您会看到偏移值 -1(在两次快照之间释放了 Leaker 对象的一个实例)。

万岁!垃圾回收有效的。现在是时候破坏它了。

内存泄漏 1:闭包

一种预防一个对象被垃圾回收的简单方式是设置一个在回调中引用该对象的间隔或超时。要查看实际应用,可更新 leaker.js 类,如清单 4 所示。

清单 4. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        this._interval = null;
        this.start();
    },

    start: function(){
        var self = this;
        this._interval = setInterval(function(){
            self.onInterval();
        }, 100);
    },

    destroy: function(){
        if(this._interval !== null){
            clearInterval(this._interval);          
        }
    },

    onInterval: function(){
        console.log("Interval");
    }
};

现在,当重复 上一节 中的第 1-9 步时,您应在第三个快照中看到,Leaker 对象被持久化,并且该间隔会永远继续运行。那么发生了什么?在一个闭包中引用的任何局部变量都会被该闭包保留,只要该闭包存在就永远保留。要确保对 setInterval 方法的回调在访问 Leaker 实例的范围时执行,需要将 this 变量分配给局部变量 self,这个变量用于从闭包内触发 onInterval。当 onInterval 触发时,它就能够访问Leaker 对象中的任何实例变量(包括它自身)。但是,只要事件侦听器存在,Leaker 对象就不会被垃圾回收。

要解决此问题,可在清空所存储的 leaker 对象引用之前,触发添加到该对象的 destroy

ヒープ プロファイラーをフィルターして、特定のクラスのインスタンスのみを表示することができます。この機能を利用するには、リークするオブジェクトの動作をカプセル化する新しいクラスを作成します。リスト 2 に示すように、このクラスはヒープ プロファイラーで簡単に見つかります。
リスト 2.assets/scripts/leaker.js

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

🎜🎜Start ボタンをバインドして Leaker オブジェクトを初期化し、それをグローバル名前空間の変数に割り当てます。また、リスト 3 に示すように、Leaker オブジェクトをクリーンアップしてガベージ コレクションの準備をするメソッドに Destroy ボタンをバインドする必要もあります。 🎜🎜リスト 3.assets/scripts/main.js🎜🎜🎜
var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        console.log("Leaking an object: %o", this);
    },

    destroy: function(){

    }      
};
🎜🎜🎜 これで、オブジェクトを作成し、メモリ内で表示して、解放する準備が整いました。 🎜
  1. 🎜 Chrome にインデックス ページを読み込みます。 Google から直接 jQuery を読み込んでいるため、このサンプルを実行するにはインターネット接続が必要です。 🎜
  2. 🎜「表示」メニューを開き、「開発」サブメニューを選択して開発者ツールを開きます。 [開発者ツール] コマンドを選択します。 🎜
  3. 🎜 図 2 に示すように、[プロファイル] タブに移動し、ヒープ スナップショットを取得します。
    🎜🎜図 2. [プロファイル] タブ🎜🎜Google Chrome スナップショットプロフィール タブの「 width=🎜
  4. 🎜ウェブに注意を戻し、開始を選択します。 🎜
  5. 🎜 別のヒープ スナップショットを取得します。 🎜
  6. 🎜最初のスナップショットをフィルタリングして Leaker クラスのインスタンスを探しますが、インスタンスは見つかりません。 2 番目のスナップショットに切り替えると、図 3 に示すようなインスタンスが見つかるはずです。
    🎜🎜図 3. スナップショットの例🎜🎜ヒープ プロファイラーのフィルタリングブラウザ ページのスナップショット🎜
  7. 🎜注意をウェブに戻し、破棄を選択します。 🎜
  8. 🎜 3 番目のヒープ スナップショットを取得します。 🎜
  9. 🎜 3 番目のスナップショットをフィルタリングして、Leaker クラスのインスタンスを探しますが、インスタンスは見つかりません。 3 番目のスナップショットをロードするときに、分析モードを概要から比較に切り替えて、3 番目と 2 番目のスナップショットを比較することもできます。オフセット値 -1 が表示されます (Leaker オブジェクトのインスタンスがスナップショット間で解放されました)。 🎜
🎜長生きしてください!ガベージコレクションは効果的です。今こそそれを破壊する時です。 🎜🎜メモリ リーク 1: クロージャ 🎜🎜 オブジェクトがガベージ コレクションされるのを防ぐ簡単な方法は、コールバックでオブジェクトを参照する間隔またはタイムアウトを設定することです。実際の動作を確認するには、リスト 4 に示すように、leaker.js クラスを更新します。 🎜🎜リスト 4.assets/scripts/leaker.js🎜🎜🎜
var Leaker = function(){};

Leaker.prototype = {
    init:function(name, parent){
        this._name = name;
        this._parent = parent;
        this._child = null;
        this.createChildren();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create a child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this);
    },

    destroy: function(){

    }
};
🎜🎜🎜 さて、前のセクションのステップ 1 ~ 9 を繰り返すと、3 番目のスナップショットに Leaker オブジェクトが表示されるはずです。は持続し、間隔は永久に実行され続けます。どうしたの?クロージャ内で参照されるローカル変数は、クロージャが存在する限り、クロージャによって保持されます。 Leaker インスタンスのスコープにアクセスしたときに setInterval メソッドへのコールバックが確実に実行されるようにするには、this 変数をローカル変数 self に割り当てる必要があります。 は、クロージャ内から onInterval をトリガーするために使用されます。 onInterval が起動すると、Leaker オブジェクト (それ自体を含む) 内の任意のインスタンス変数にアクセスできます。ただし、イベント リスナーが存在する限り、Leaker オブジェクトはガベージ コレクションされません。 🎜🎜この問題を解決するには、保存されている leaker オブジェクト参照をクリアする前に、次に示すように、Destroy ボタン プログラムのクリック ハンドラーを更新して、オブジェクトに追加された destroy メソッドをトリガーします。リスト 5 にあります。 🎜🎜リスト 5.assets/scripts/main.js🎜🎜🎜
leak = new Leaker(); 
leak.init("leaker 1", null);
🎜🎜

销毁对象和对象所有权

一种不错的做法是,创建一个标准方法来负责让一个对象有资格被垃圾回收。destroy 功能的主要用途是,集中清理该对象完成的具有以下后果的操作的职责:

  • 阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。

  • 使用不必要的 CPU 周期,比如间隔或动画。

destroy 方法常常是清理一个对象的必要步骤,但在大多数情况下它还不够。在理论上,在销毁相关实例后,保留对已销毁对象的引用的其他对象可调用自身之上的方法。因为这种情形可能会产生不可预测的结果,所以仅在对象即将无用时调用 destroy 方法,这至关重要。

一般而言,destroy 方法最佳使用是在一个对象有一个明确的所有者来负责它的生命周期时。此情形常常存在于分层系统中,比如 MVC 框架中的视图或控制器,或者一个画布呈现系统的场景图。

内存泄漏 2:控制台日志

一种将对象保留在内存中的不太明显的方式是将它记录到控制台中。清单 6 更新了 Leaker 类,显示了此方式的一个示例。

清单 6. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        console.log("Leaking an object: %o", this);
    },

    destroy: function(){

    }      
};

可采取以下步骤来演示控制台的影响。

  1. 登录到索引页面。

  2. 单击 Start

  3. 转到控制台并确认 Leaking 对象已被跟踪。

  4. 单击 Destroy

  5. 回到控制台并键入 leak,以记录全局变量当前的内容。此刻该值应为空。

  6. 获取另一个堆快照并过滤 Leaker 对象。您应留下一个 Leaker 对象。

  7. 回到控制台并清除它。

  8. 创建另一个堆配置文件。在清理控制台后,保留 leaker 的配置文件应已清除。

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:

  • 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。

  • 由 console.log 和 console.dir 方法记录的对象。

内存泄漏 3:循环

在两个对象彼此引用且彼此保留时,就会产生一个循环,如图 4 所示。

图 4. 创建一个循环的引用

该图中的一个蓝色 root 节点连接到两个绿色框,显示了它们之间的一个连接

清单 7 显示了一个简单的代码示例。

清单 7. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(name, parent){
        this._name = name;
        this._parent = parent;
        this._child = null;
        this.createChildren();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create a child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this);
    },

    destroy: function(){

    }
};

Root 对象的实例化可以修改,如清单 8 所示。

清单 8. assets/scripts/main.js

leak = new Leaker(); 
leak.init("leaker 1", null);

如果在创建和销毁对象后执行一次堆分析,您应该会看到垃圾收集器检测到了这个循环引用,并在您选择 Destroy 按钮时释放了内存。

但是,如果引入了第三个保留该子对象的对象,该循环会导致内存泄漏。例如,创建一个 registry 对象,如清单 9 所示。

清单 9. assets/scripts/registry.js

var Registry = function(){};

Registry.prototype = {
    init:function(){
        this._subscribers = [];
    },

    add:function(subscriber){
        if(this._subscribers.indexOf(subscriber) >= 0){
            // Already registered so bail out
            return;
        }
        this._subscribers.push(subscriber);
    },

    remove:function(subscriber){
        if(this._subscribers.indexOf(subscriber) < 0){
            // Not currently registered so bail out
            return;
        }
              this._subscribers.splice(
                  this._subscribers.indexOf(subscriber), 1
              );
    }
};

registry 类是让其他对象向它注册,然后从注册表中删除自身的对象的简单示例。尽管这个特殊的类与注册表毫无关联,但这是事件调度程序和通知系统中的一种常见模式。

将该类导入 index.html 页面中,放在 leaker.js 之前,如清单 10 所示。

清单 10. index.html

<script src="assets/scripts/registry.js" type="text/javascript" 
charset="utf-8"></script>

更新 Leaker 对象,以向注册表对象注册该对象本身(可能用于有关一些未实现事件的通知)。这创建了一个来自要保留的 leaker 子对象的 root 节点备用路径,但由于该循环,父对象也将保留,如清单 11 所示。

清单 11. assets/scripts/leaker.js

var Leaker = function(){};
Leaker.prototype = {

    init:function(name, parent, registry){
        this._name = name;
        this._registry = registry;
        this._parent = parent;
        this._child = null;
        this.createChildren();
        this.registerCallback();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this, this._registry);
    },

    registerCallback:function(){
        this._registry.add(this);
    },

    destroy: function(){
        this._registry.remove(this);
    }
};

最后,更新 main.js 以设置注册表,并将对注册表的一个引用传递给 leaker 父对象,如清单 12 所示。

清单 12. assets/scripts/main.js

	  $("#start_button").click(function(){
  var leakExists = !(
	      window["leak"] === null || window["leak"] === undefined
	  );
  if(leakExists){
      return;
  }
  leak = new Leaker();
  leak.init("leaker 1", null, registry);
});

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

registry = new Registry();
registry.init();

现在,当执行堆分析时,您应看到每次选择 Start 按钮时,会创建并保留 Leaker 对象的两个新实例。图 5 显示了对象引用的流程。

图 5. 由于保留引用导致的内存泄漏

3 个方框显示了 root 节点与父和子对象之间的 3 个不同路径

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 5 的模式。这种类型的模式也可能与闭包和控制台日志导致的问题相关联。

尽管有多种方式来解决此类问题,但在此情况下,最简单的方式是更新 Leaker 类,以在销毁它时销毁它的子对象。对于本示例,更新destroy 方法(如清单 13 所示)就足够了。

清单 13. assets/scripts/leaker.js

destroy: function(){
    if(this._child !== null){
        this._child.destroy();            
    }
    this._registry.remove(this);
}

有时,两个没有足够紧密关系的对象之间也会存在循环,其中一个对象管理另一个对象的生命周期。在这样的情况下,在这两个对象之间建立关系的对象应负责在自己被销毁时中断循环。

结束语

即使 JavaScript 已被垃圾回收,仍然会有许多方式会将不需要的对象保留在内存中。目前大部分浏览器都已改进了内存清理功能,但评估您应用程序内存堆的工具仍然有限(除了使用 Google Chrome)。通过从简单的测试案例开始,很容易评估潜在的泄漏行为并确定是否存在泄漏。

不经过测试,就不可能准确度量内存使用。很容易使循环引用占据对象曲线图中的大部分区域。Chrome 的 Heap Profiler 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。


以上がJavaScript プログラムのメモリ リークのグラフィック コードの詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。