目录
简介
对象生命周期
图 1. 通过垃圾收集回收内存
分析堆快照
清单 10. index.html
清单 13. assets/scripts/leaker.js
清单 12. assets/scripts/main.js
图 2. Profiles 选项卡
图 3. 快照实例
内存泄漏 1:闭包
销毁对象和对象所有权
内存泄漏 2:控制台日志
内存泄漏 3:循环
图 4. 创建一个循环的引用
清单 9. assets/scripts/registry.js
图 5. 由于保留引用导致的内存泄漏
结束语
首页 web前端 js教程 JavaScript 程序中内存泄漏的图文代码详细介绍

JavaScript 程序中内存泄漏的图文代码详细介绍

Mar 09, 2017 pm 02:11 PM

JavaScript 程序中内存泄漏的图文代码详细介绍

垃圾回收解放了我们,它让我们可将精力集中在应用程序逻辑(而不是内存管理)上。但是,垃圾收集并不神奇。了解它的工作原理,以及如何使它保留本应在很久以前释放的内存,就可以实现更快更可靠的应用程序。在本文中,学习一种定位 JavaScript 应用程序中内存泄漏的系统方法、几种常见的泄漏模式,以及解决这些泄漏的适当方法。

简介

当处理 JavaScript 这样的脚本语言时,很容易忘记每个对象、类、字符串、数字和方法都需要分配和保留内存。语言和运行时的垃圾回收器隐藏了内存分配和释放的具体细节。

许多功能无需考虑内存管理即可实现,但却忽略了它可能在程序中带来重大的问题。不当清理的对象可能会存在比预期要长得多的时间。这些对象继续响应事件和消耗资源。它们可强制浏览器从一个虚拟磁盘驱动器分配内存页,这显著影响了计算机的速度(在极端的情形中,会导致浏览器崩溃)。

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。在最近几年中,许多浏览器都改善了在页面加载过程中从 JavaScript 回收内存的能力。但是,并不是所有浏览器都具有相同的运行方式。Firefox 和旧版的 Internet Explorer 都存在过内存泄漏,而且内存泄露一直持续到浏览器关闭。

过去导致内存泄漏的许多经典模式在现代浏览器中以不再导致泄漏内存。但是,如今有一种不同的趋势影响着内存泄漏。许多人正设计用于在没有硬页面刷新的单页中运行的 Web 应用程序。在那样的单页中,从应用程序的一个状态到另一个状态时,很容易保留不再需要或不相关的内存。

在本文中,了解对象的基本生命周期,垃圾回收如何确定一个对象是否被释放,以及如何评估潜在的泄漏行为。另外,学习如何使用 Google Chrome 中的 Heap Profiler 来诊断内存问题。一些示例展示了如何解决闭包、控制台日志和循环带来的内存泄漏。

对象生命周期

要了解如何预防内存泄漏,需要了解对象的基本生命周期。当创建一个对象时,JavaScript 会自动为该对象分配适当的内存。从这一刻起,垃圾回收器就会不断对该对象进行评估,以查看它是否仍是有效的对象。

垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。图 1 显示了垃圾回收器回收内存的一个示例。

图 1. 通过垃圾收集回收内存

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

看到该系统的实际应用会很有帮助,但提供此功能的工具很有限。了解您的 JavaScript 应用程序占用了多少内存的一种方式是使用系统工具查看浏览器的内存分配。有多个工具可为您提供当前的使用,并描绘一个进程的内存使用量随时间变化的趋势图。

例如,如果在 Mac OSX 上安装了 XCode,您可以启动 Instruments 应用程序,并将它的活动监视器工具附加到您的浏览器上,以进行实时分析。在 Windows® 上,您可以使用任务管理器。如果在您使用应用程序的过程中,发现内存使用量随时间变化的曲线稳步上升,那么您就知道存在内存泄漏。

观察浏览器的内存占用只能非常粗略地显示 JavaScript 应用程序的实际内存使用。浏览器数据不会告诉您哪些对象发生了泄漏,也无法保证数据与您应用程序的真正内存占用确实匹配。而且,由于一些浏览器中存在实现问题,DOM 元素(或备用的应用程序级对象)可能不会在页面中销毁相应元素时释放。视频标记尤为如此,视频标记需要浏览器实现一种更加精细的基础架构。

人们曾多次尝试在客户端 JavaScript 库中添加对内存分配的跟踪。不幸的是,所有尝试都不是特别可靠。例如,流行的 stats.js 包由于不准确性而无法支持。一般而言,尝试从客户端维护或确定此信息存在一定的问题,是因为它会在应用程序中引入开销且无法可靠地终止。

理想的解决方案是浏览器供应商在浏览器中提供一组工具,帮助您监视内存使用,识别泄漏的对象,以及确定为什么一个特殊对象仍标记为保留。

目前,只有 Google Chrome(提供了 Heap Profile)实现了一个内存管理工具作为它的开发人员工具。我在本文中使用 Heap Profiler 测试和演示 JavaScript 运行时如何处理内存。

分析堆快照

在创建内存泄漏之前,请查看一次适当收集内存的简单交互。首先创建一个包含两个按钮的简单 HTML 页面,如清单 1 所示。

清单 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 方法添加脚本标记。在开发环境中,将 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 方法,方法是更新 Destroy 按钮的单击处理程序,如清单 5 所示。

清单 5. assets/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.logconsole.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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Go 内存泄漏追踪:Go pprof 实操指南 Go 内存泄漏追踪:Go pprof 实操指南 Apr 08, 2024 am 10:57 AM

pprof工具可用于分析Go应用程序的内存使用情况和检测内存泄漏。它提供内存概况生成、内存泄漏识别和实时分析功能。通过使用pprof.Parse生成内存快照,并使用pprof-allocspace命令识别内存分配最多的数据结构。同时,pprof支持实时分析,并提供端点以远程访问内存使用情况信息。

解决闭包导致的内存泄漏问题 解决闭包导致的内存泄漏问题 Feb 18, 2024 pm 03:20 PM

标题:闭包引起的内存泄漏及解决方法引言:闭包是JavaScript中一个非常常见的概念,它可以让内部函数访问外部函数的变量。然而,闭包在使用不当的情况下可能导致内存泄漏。本文将探讨闭包引起的内存泄漏问题,并提供解决方法及具体代码示例。一、闭包引起的内存泄漏问题闭包的特性是内部函数可以访问外部函数的变量,这意味着在闭包中引用的变量不会被垃圾回收。如果使用不当,

简易JavaScript教程:获取HTTP状态码的方法 简易JavaScript教程:获取HTTP状态码的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教程:如何获取HTTP状态码,需要具体代码示例前言:在Web开发中,经常会涉及到与服务器进行数据交互的场景。在与服务器进行通信时,我们经常需要获取返回的HTTP状态码来判断操作是否成功,根据不同的状态码来进行相应的处理。本篇文章将教你如何使用JavaScript获取HTTP状态码,并提供一些实用的代码示例。使用XMLHttpRequest

Golang 技术性能优化中如何避免内存泄漏? Golang 技术性能优化中如何避免内存泄漏? Jun 04, 2024 pm 12:27 PM

内存泄漏会导致Go程序内存不断增加,可通过:关闭不再使用的资源,如文件、网络连接和数据库连接。使用弱引用防止内存泄漏,当对象不再被强引用时将其作为垃圾回收目标。利用go协程,协程栈内存会在退出时自动释放,避免内存泄漏。

如何使用Valgrind检测内存泄漏? 如何使用Valgrind检测内存泄漏? Jun 05, 2024 am 11:53 AM

Valgrind通过模拟内存分配和释放来检测内存泄漏和错误,使用步骤如下:安装Valgrind:从官方网站下载并安装适用于您操作系统的版本。编译程序:使用Valgrind标志(如gcc-g-omyprogrammyprogram.c-lstdc++)编译程序。分析程序:使用valgrind--leak-check=fullmyprogram命令分析已编译的程序。检查输出:Valgrind将在程序执行后生成报告,显示内存泄漏和错误信息。

C++ 中内存泄漏的调试技术 C++ 中内存泄漏的调试技术 Jun 05, 2024 pm 10:19 PM

C++中内存泄漏是指程序分配了内存但忘记释放,导致内存无法被重用。调试技术包括使用调试器(如Valgrind、GDB)、插入断言和使用内存泄漏检测器库(如Boost.LeakDetector、MemorySanitizer)。通过实践案例展示了使用Valgrind检测内存泄漏,并提出了避免内存泄漏的最佳做法,包括:始终释放分配的内存、使用智能指针、使用内存管理库和定期进行内存检查。

闭包中如何有效避免内存泄漏? 闭包中如何有效避免内存泄漏? Jan 13, 2024 pm 12:46 PM

如何在闭包中阻止内存泄漏的发生?闭包是JavaScript中非常强大的特性之一,它能够实现函数的嵌套和数据的封装。然而,闭包也容易导致内存泄漏的问题,特别是在处理异步和定时器的情况下。本文将介绍如何在闭包中阻止内存泄漏,并提供具体的代码示例。内存泄漏通常发生在不再需要某个对象时,却因为某些原因无法释放其所占用的内存。在闭包中,当函数引用外部的变量,而这些变量

如何在JavaScript中获取HTTP状态码的简单方法 如何在JavaScript中获取HTTP状态码的简单方法 Jan 05, 2024 pm 01:37 PM

JavaScript中的HTTP状态码获取方法简介:在进行前端开发中,我们常常需要处理与后端接口的交互,而HTTP状态码就是其中非常重要的一部分。了解和获取HTTP状态码有助于我们更好地处理接口返回的数据。本文将介绍使用JavaScript获取HTTP状态码的方法,并提供具体代码示例。一、什么是HTTP状态码HTTP状态码是指当浏览器向服务器发起请求时,服务

See all articles