【转贴】了解 JavaScript 应用程序中的内存泄漏

转贴:http://www.ibm.com/developerworks/cn/web/wa-jsmemory/

检测和解决内存问题

Ben Dolmar, 软件开发人员, The Nerdery

 

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

简介

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

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

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

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

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

您可 下载 本文中使用的示例的源代码。

 

回页首

对象生命周期

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

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

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

看到该系统的实际应用会很有帮助,但提供此功能的工具很有限。了解您的 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 选项卡
     

  4. 将注意力返回到 Web 上,选择 Start
  5. 获取另一个堆快照。
  6. 过滤第一个快照,查找 Leaker 类的实例,找不到任何实例。切换到第二个快照,您应该能找到一个实例,如图 3 所示。

    图 3. 快照实例
     

  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.log 和 console.dir 方法记录的对象。

 

回页首

内存泄漏 3:循环

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

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

清单 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. 由于保留引用导致的内存泄漏
 

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 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 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

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

 

 

回页首

下载

描述 名字 大小 下载方法
文章源代码 JavascriptMemoryManagementSource.zip 4KB HTTP

关于下载方法的信息

 

参考资料

学习

获得产品和技术

讨论

  • developerWorks 社区:查看开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户交流。

关于作者


Ben Dolmar 自 2001 年起就开始了专业编程,拥有丰富的技术经验,包括 ActionScript、iOS、JavaScript、PHP、Ruby 和图形设计。自 1997 年从威斯康星大学麦迪逊分校毕业并获得新闻和政治学双学位后,Ben 在 1998 年到 2007 年期间担任 Faith Inkubators 的产品总监。自 2007 年加入 The Nerdery 以来,他已帮助启动了 400 多个项目。在 2002 年,Ben 被提拔为首席软件工程师。他曾在 SWFCamp 进行过 ActionScript 方面的演讲,出席过在芝加哥举行的有关不断演化的 Web 标准和 HTML 5 的 SocialMediaDev Camp。

时间: 2024-12-31 14:40:24

【转贴】了解 JavaScript 应用程序中的内存泄漏的相关文章

调试内存泄漏的应用程序: 发现并防止托管代码中出现内存泄漏

本文讨论: 理解托管应用程序中的内存泄漏问题 .NET 应用程序中所用的非托管内存 帮助 .NET 垃圾收集器发挥应有功效 本文使用了以下技术: .NET Framework 目录 .NET 应用程序中的内存 检测泄漏 堆栈内存泄漏 非托管堆内存泄漏 "泄漏"托管堆内存 总结 一提到托管代码中出现内存泄漏,很多开发人员的第一反应都认为这是不可能的.毕竟垃圾收集器 (GC) 会负责管理所有的内存,没错吧?但要知道,垃圾收集器只处理托管内存.基于 Microsoft .NET Framew

JavaScript中的内存泄露模式:在JavaScript应用程序中处理循环引用

如果您知道内存泄漏的起因,那么在 JavaScript 中进行相应的防范就应该相当容易.在这篇文章中,作者 Kiran Sundar 和 Abhijeet Bhattacharya 将带您亲历 JavaScript 中的循环引用的全部基本知识,向您介绍为何它们会在某些浏览器中产生问题,尤其是在结合了闭包的情况下.在了解了您应该引起注意的常见内存泄漏模式之后,您还将学到应对这些泄漏的诸多方法. JavaScript 是用来向 Web 页面添加动态内容的一种功能强大的脚本语言.它尤其特别有助于一些日

JavaScript中的内存泄漏以及如何处理

随着现在的编程语言功能越来越成熟.复杂,内存管理也容易被大家忽略.本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题.   概述 像C语言这样的编程语言,具有简单的内存管理功能函数,例如malloc( )和free( ).开发人员可以使用这些功能函数来显式地分配和释放系统的内存. 当创建对象和字符串等时,JavaScript就会分配内存,并在不再使用时自动释放内存,这种机制被称为垃圾收集.这种释放资源看似是"自动&qu

浅析Node.js中的内存泄漏问题

  这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题

浅析Node.js中的内存泄漏问题_node.js

 这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题文

mfc-MFC中视频流内存泄漏问题

问题描述 MFC中视频流内存泄漏问题 void CHLDlg::OnBnClickedOpen(){ // TODO: 在此添加控件通知处理程序代码 CFileDialog dlg(TRUENULLNULLNULLNULL); pCapture = NULL; if(dlg.DoModal()==IDOK)// { PathName = dlg.GetPathName(); FileName = dlg.GetFileName(); } else { return; } c=(LPCSTR)Pa

分析Python中的内存泄漏

  分析Python中的内存泄漏 引子 之前一直盲目的认为 Python 不会存在内存泄露, 但是眼看着上线的项目随着运行时间的增长 而越来越大的内存占用, 我意识到我写的程序在发生内存泄露, 之前 debug 过 logging 模块导致的内存泄露. 目前看来, 还有别的地方引起的内存泄露. 经过一天的奋战, 终于找到了内存泄露的地方, 目前项目 跑了很长时间, 在业务量较小的时候内存还是能回到刚启动的时候的内存占用. 什么情况下不用这么麻烦 如果你的程序只是跑一下就退出大可不必大费周章的去查

在 JNI 编程中避免内存泄漏

简介: 本文详细论述如何在 JNI 编程中避免内存泄漏.论述了 JNI 编程中可能引发的明显的内存泄漏.本文的重点是阐述 JNI 编程中潜在的内存泄漏,希望读者通过本文对 Local reference 有更深刻的理解,了解 Local reference 表的存在,区分 Local reference 和局部变量,从而认识到 Local reference 可能引发的 native memory 内存泄漏 JNI 编程简介 JNI,Java Native Interface,是 native

Java中关于内存泄漏出现的原因汇总及如何避免内存泄漏(超详细版)_java

Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题.内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收.最近自己阅读了大量相关的文档资料,打算做个 总结 沉淀下来跟大家一起分享和学习,也给自己一个警示,以后 coding 时怎么避免这些情况,提高应用的体验和质量. 我会从 java 内存泄漏的基础知识开始,并通过具体例子来说明 Android 引起内存泄漏的各种原因,以