Heim  >  Artikel  >  Web-Frontend  >  Detaillierte Einführung in den grafischen Code von Speicherlecks in JavaScript-Programmen

Detaillierte Einführung in den grafischen Code von Speicherlecks in JavaScript-Programmen

黄舟
黄舟Original
2017-03-09 14:11:011450Durchsuche

Detaillierte Einführung in den grafischen Code von Speicherlecks in JavaScript-Programmen:

Garbage Collection befreit uns und ermöglicht uns, uns auf die Anwendungslogik zu konzentrieren (nicht Speicherverwaltung). Allerdings ist die Speicherbereinigung keine Zauberei. Wenn Sie verstehen, wie es funktioniert und wie Sie dafür sorgen können, dass der Speicher erhalten bleibt, der schon vor langer Zeit hätte freigegeben werden sollen, kann dies zu schnelleren und zuverlässigeren Anwendungen führen. In diesem Artikel lernen Sie einen systematischen Ansatz zum Auffinden von Speicherlecks in JavaScript-Anwendungen, mehrere gängige Leckmuster und die geeigneten Methoden zur Behebung dieser Lecks kennen.

Einführung

Beim Umgang mit einer Skriptsprache wie JavaScript vergisst man leicht, dass jedes Objekt, jede Klasse, jeder String, jede Zahl und jede Methode Speicher zuweisen und reservieren muss. Die spezifischen Details der Speicherzuweisung und -freigabe sind den Sprach- und Laufzeit-Garbage Collectors verborgen.

Viele Funktionen können implementiert werden, ohne die Speicherverwaltung zu berücksichtigen, aber das Ignorieren kann zu erheblichen Problemen im Programm führen. Unsachgemäß gereinigte Gegenstände können viel länger als erwartet bestehen bleiben. Diese Objekte reagieren weiterhin auf Ereignisse und verbrauchen Ressourcen. Sie zwingen den Browser, Speicherseiten von einem virtuellen Laufwerk zu reservieren, was den Computer erheblich verlangsamt (und im Extremfall zum Absturz des Browsers führt).

Ein Speicherverlust ist jedes Objekt, das bestehen bleibt, nachdem Sie es nicht mehr besitzen oder benötigen. In den letzten Jahren haben viele Browser ihre Fähigkeit verbessert, beim Laden von Seiten Speicher von JavaScript zurückzugewinnen. Allerdings funktionieren nicht alle Browser gleich. Sowohl bei Firefox als auch bei älteren Versionen von Internet Explorer sind Speicherlecks aufgetreten, die bestehen bleiben, bis der Browser geschlossen wird.

Viele klassische Muster, die in der Vergangenheit zu Speicherverlusten geführt haben, verursachen in modernen Browsern keine Speicherverluste mehr. Allerdings gibt es heute einen anderen Trend, der sich auf Speicherlecks auswirkt. Viele entwerfen Webanwendungen so, dass sie auf einer einzigen Seite ohne harte Seitenaktualisierungen ausgeführt werden. Auf einer einzelnen Seite wie dieser lässt sich leicht Speicher behalten, der nicht mehr benötigt oder relevant ist, wenn man von einem Status der Anwendung in einen anderen wechselt.

In diesem Artikel erfahren Sie mehr über den grundlegenden Lebenszyklus von Objekten, wie die Garbage Collection bestimmt, ob ein Objekt freigegeben wurde, und wie man potenzielles Leckageverhalten bewertet. Erfahren Sie außerdem, wie Sie Heap Profiler in Google Chrome verwenden, um Speicherprobleme zu diagnostizieren. Einige Beispiele zeigen, wie Speicherlecks behoben werden können, die durch Abschlüsse, Konsolenprotokolle und Schleifen verursacht werden.

Objektlebenszyklus

Um zu verstehen, wie Speicherlecks verhindert werden können, müssen Sie den grundlegenden Lebenszyklus von Objekten verstehen. Wenn ein Objekt erstellt wird, weist JavaScript dem Objekt automatisch den entsprechenden Speicher zu. Von diesem Moment an wertet der Garbage Collector das Objekt kontinuierlich aus, um festzustellen, ob es noch ein gültiges Objekt ist.

Der Garbage Collector scannt regelmäßig Objekte und zählt die Anzahl anderer Objekte, die Verweise auf jedes Objekt haben. Wenn ein Objekt 0 Referenzen hat (keine anderen Objekte verweisen darauf) oder die einzige Referenz auf das Objekt zirkulär ist, kann der Speicher des Objekts zurückgefordert werden. Abbildung 1 zeigt ein Beispiel für die Speicherrückgewinnung durch den Garbage Collector.

Abbildung 1. Speicherrückgewinnung durch Garbage Collection

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

Es wäre hilfreich, dieses System in Aktion zu sehen, aber die Tools, die diese Funktionalität bieten, sind begrenzt. Eine Möglichkeit herauszufinden, wie viel Speicher Ihre JavaScript-Anwendung beansprucht, besteht darin, mithilfe von Systemtools die Speicherzuweisungen Ihres Browsers zu überprüfen. Es gibt mehrere Tools, die Ihnen aktuelle Nutzungs- und Trenddiagramme der Speichernutzung eines Prozesses im Zeitverlauf liefern können.

Wenn Sie beispielsweise XCode unter Mac OSX installiert haben, können Sie die Instruments-App starten und das Aktivitätsmonitor-Tool zur Echtzeitanalyse an Ihren Browser anhängen. Unter Windows® können Sie den Task-Manager verwenden. Sie wissen, dass ein Speicherverlust vorliegt, wenn Sie bei der Verwendung Ihrer Anwendung im Laufe der Zeit einen stetigen Anstieg der Speichernutzung feststellen.

Die Beobachtung des Speicherbedarfs des Browsers gibt nur einen sehr groben Hinweis auf die tatsächliche Speichernutzung einer JavaScript-Anwendung. Durch Browserdaten erfahren Sie nicht, welche Objekte durchgesickert sind, und es gibt keine Garantie dafür, dass die Daten tatsächlich mit dem tatsächlichen Speicherbedarf Ihrer Anwendung übereinstimmen. Aufgrund von Implementierungsproblemen in einigen Browsern werden DOM-Elemente (oder alternative Objekte auf Anwendungsebene) möglicherweise nicht freigegeben, wenn das entsprechende Element auf der Seite zerstört wird. Dies gilt insbesondere für Video-Tags, bei denen Browser eine aufwändigere Infrastruktur implementieren müssen.

Es gab mehrere Versuche, die Verfolgung von Speicherzuweisungen in clientseitigen JavaScript-Bibliotheken hinzuzufügen. Leider war keiner der Versuche besonders zuverlässig. Beispielsweise wird das beliebte Paket stats.js aufgrund von Ungenauigkeiten nicht unterstützt. Im Allgemeinen ist der Versuch, diese Informationen vom Client zu verwalten oder zu ermitteln, problematisch, da dies zu einem Mehraufwand für die Anwendung führt und nicht zuverlässig beendet werden kann.

Die ideale Lösung wäre, dass der Browser-Anbieter eine Reihe von Tools im Browser bereitstellt, mit denen Sie die Speichernutzung überwachen, durchgesickerte Objekte identifizieren und feststellen können, warum ein bestimmtes Objekt immer noch als reserviert markiert ist.

Derzeit implementiert nur Google Chrome (das Heap Profile bereitstellt) ein Speicherverwaltungstool als Entwicklertools. In diesem Artikel verwende ich Heap Profiler, um zu testen und zu demonstrieren, wie die JavaScript-Laufzeit mit Speicher umgeht.

Analysieren von Heap-Snapshots

Bevor Sie einen Speicherverlust verursachen, überprüfen Sie eine einfache Interaktion, die Speicher ordnungsgemäß sammelt. Erstellen Sie zunächst eine einfache HTML-Seite mit zwei Schaltflächen, wie in Listing 1 gezeigt.

Listing 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>

Die Einbeziehung von jQuery soll eine einfache Syntax für die Verwaltung von Ereignisbindungen gewährleisten, die für verschiedene Browser geeignet ist. und halten Sie sich strikt an die gängigsten Entwicklungspraktiken. Fügen Sie der leaker-Klasse und den wichtigsten JavaScript-Methoden Skript-Tags hinzu. In einer Entwicklungsumgebung ist es oft besser, JavaScript-Dateien in einer einzigen Datei zusammenzufassen. Für dieses Beispiel ist es einfacher, die Logik in einer separaten Datei abzulegen.

Sie können den Heap-Profiler filtern, um nur Instanzen einer bestimmten Klasse anzuzeigen. Um diese Funktion zu nutzen, erstellen Sie eine neue Klasse, die das Verhalten von Leckobjekten kapselt. Diese Klasse ist leicht im Heap Profiler zu finden, wie in Listing 2 gezeigt.

Listing 2. asset/scripts/leaker.js

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

    }    
};

Binden Sie die Start-Schaltfläche, um das Leaker-Objekt zu initialisieren, und Es wird einer Variablen im globalen Namensraum zugewiesen. Sie müssen die Schaltfläche „Zerstören“ außerdem an eine Methode binden, die das Leaker-Objekt bereinigen und für die Garbage Collection vorbereiten soll, wie in Listing 3 gezeigt.

Listing 3. asset/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();

Jetzt sind Sie bereit, ein Objekt im Speicher zu erstellen. Schauen Sie es sich an und dann loslassen.

  1. Laden Sie die Indexseite in Chrome. Da Sie jQuery direkt von Google laden, benötigen Sie eine Internetverbindung, um dieses Beispiel auszuführen.

  2. Öffnen Sie die Entwicklertools, indem Sie das Menü „Ansicht“ öffnen und das Untermenü „Entwickeln“ auswählen. Wählen Sie den Befehl Entwicklertools.

  3. Gehen Sie zur Registerkarte Profile und erstellen Sie einen Heap-Snapshot, wie in Abbildung 2 dargestellt.

    Abbildung 2. Registerkarte „Profile“

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

  4. Wenden Sie Ihre Aufmerksamkeit wieder dem Web und wählen Sie Starten .

  5. Machen Sie einen weiteren Heap-Schnappschuss.

  6. Filtern Sie den ersten Snapshot und suchen Sie nach Instanzen der Klasse Leaker. Es werden keine Instanzen gefunden. Wechseln Sie zum zweiten Snapshot und Sie sollten eine Instanz finden, wie in Abbildung 3 dargestellt.

    Abbildung 3. Snapshot-Beispiel

    Heap Profiler 过滤器页面的快照

  7. Wenden Sie Ihre Aufmerksamkeit wieder auf das Web und wählen Sie Zerstören .

  8. Machen Sie den dritten Heap-Schnappschuss.

  9. Filtern Sie den dritten Snapshot und suchen Sie nach Instanzen der Klasse Leaker. Es werden keine Instanzen gefunden. Beim Laden des dritten Snapshots können Sie auch den Analysemodus von Zusammenfassung auf Vergleich umstellen und den dritten und zweiten Snapshot vergleichen. Sie sehen einen Offsetwert von -1 (eine Instanz des Leaker-Objekts wurde zwischen Snapshots freigegeben).

Lang lebe! Die Müllabfuhr ist effektiv. Jetzt ist es an der Zeit, es zu zerstören.

Speicherleck 1: Schließungen

Eine einfache Möglichkeit, zu verhindern, dass ein Objekt durch Garbage Collection erfasst wird, besteht darin, ein Intervall oder eine Zeitüberschreitung für die Referenzierung des Objekts in Rückrufen festzulegen. Um es in Aktion zu sehen, aktualisieren Sie die Klasse „leaker.js“, wie in Listing 4 gezeigt.

Listing 4. asset/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");
    }
};

Nun, wenn Sie die Schritte 1-9 aus dem vorherigen Abschnitt wiederholen. Wann Wenn Sie den Schritt ausführen, sollten Sie im dritten Schnappschuss sehen, dass das Leaker-Objekt bestehen bleibt und das Intervall für immer weiterläuft. Was ist also passiert? Alle lokalen Variablen, auf die innerhalb eines Abschlusses verwiesen wird, werden vom Abschluss beibehalten, solange der Abschluss existiert. Um sicherzustellen, dass der Rückruf auf die Methode setInterval ausgeführt wird, wenn auf den Bereich der Leaker-Instanz zugegriffen wird, muss die Variable this der lokalen Variablen self zugewiesen werden, die zum Auslösen von onInterval von innen verwendet wird die Schließung. Wenn onInterval ausgelöst wird, kann es auf jede Instanzvariable im Leaker-Objekt zugreifen (einschließlich sich selbst). Solange der Ereignis-Listener jedoch vorhanden ist, wird das Leaker-Objekt nicht durch Garbage Collection erfasst.

Um dieses Problem zu beheben, lösen Sie die leaker-Methode aus, die der gespeicherten destroy-Objektreferenz hinzugefügt wurde, bevor Sie das Objekt löschen, indem Sie den Klickhandler der Schaltfläche „Zerstören“ aktualisieren, wie in Listing 5 Show gezeigt.

Listing 5. asset/scripts/main.js

$("#destroy_button").click(function(){
    leak.destroy();
    leak = 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 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

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


Das obige ist der detaillierte Inhalt vonDetaillierte Einführung in den grafischen Code von Speicherlecks in JavaScript-Programmen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn