关于内存泄漏

概述

像 C 语言,拥有底层原始的内存管理方法,例如:malloc() 和 free()。这些原始的方法被开发者用来从操做系统中分配内存和释放内存。javascript

然而,JavaScript 当一些东西(objects,strings,etc.)被建立的时候分配内存而且当它们再也不被使用的时候“自动”释放它们,这个过程被称为垃圾回收。java

释放资源的这种看似“自动”的性质是形成困扰的根源。它给 JavaScript (和其它高级语言)开发者一个错误的印象——他们能够选择不关心内存管理。这是一个很大的错误。算法

即便是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便可以正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)编程

 

关于内存数组

 

不少东西都存储在内存中:浏览器

 

  1. 全部程序使用的全部变量和其余数据。
  2. 程序的代码,包括操做系统的。

编译代码时,编译器能够检查原始数据类型,并提早计算出须要多少内存。而后将所需的数量分配给调用堆栈中的程序。这些变量分配的空间称为堆栈空间,由于随着函数被调用,它们的内存被添加到现有存储器的顶部。当它们终止时,它们以 LIFO (last-in,first-out)顺序被移除。静态和动态分配内存数据结构

它不能为堆栈上的变量分配空间。相反,咱们的程序须要在运行时明确地要求操做系统得到适当的空间量。这个内存时从堆空间分配的。静态和动态内存分配的区别以下表所示:闭包

Static allocation Dynamic allocation
编译时内存大小肯定 编译时内存大小不肯定
编译阶段执行 运行时执行
分配给栈 分配给堆
FILO 没有特定的顺序

JavaScript 中使用内存

基本上在 JavaScript 中使用内存的意思就是在内存在进行 读 和 写。框架

这个操做多是一个变量值的读取或写入,一个对象属性的读取或写入,甚至时向函数中传递参数。dom

什么是内存泄漏

实质上,内存泄漏能够被定义为应用程序再也不须要的内存,但因为某种缘由,内存不会返回到操做系统或可用内存池中。

编程语言支持多种管理内存的方法。然而,某块内存是否被使用其实是一个不肯定的问题。换句话说,只有开发人员能够清楚一块内存是否能够释放到操做系统又或者不应被释放。

某些编程语言提供了一些特性,帮助开发者处理这些事情。另一些语言指望开发者可以彻底本身去明确地控制内存。维基百科有关于手动和自动内存管理的好文章。

四种常见的 JavaScript 内存泄漏

1:Global variables

JavaScript 以有趣的方式处理未声明的变量:对未声明的变量的引用在全局对象内建立一个新变量。在浏览器中,全局对象就是 window。换种说法:

  1.  
    function foo(arg) {
  2.  
    bar = "some text";
  3.  
    }

等价于:

  1.  
    function foo(arg) {
  2.  
    window.bar = "some text";
  3.  
    }

若是 bar 被假定为仅仅在函数 foo 的做用域范围内持有对变量的引用,可是你却忘记了使用 var 来声明它,那么就会建立一个意外的全局变量。

在这个例子中,泄漏一个简单的字符串不会有太大的伤害,但确定会变得更糟的。

能够经过另外一种方式建立意外的全局变量:

  1.  
    function foo() {
  2.  
    this.var1 = "potential accidental global";
  3.  
    }
  4.  
    // Foo called on its own, this points to the global object (window)
  5.  
    // rather than being undefined.
  6.  
    foo();

为了防止这些错误的发生,能够在 JavaScript 文件开头添加 “use strict”,使用严格模式。这样在严格模式下解析 JavaScript 能够防止意外的全局变量。

即便咱们讨论了如何预防意外全局变量的产生,可是仍然会有不少代码用显示的方式去使用全局变量。这些全局变量是没法进行垃圾回收的(除非将它们赋值为 null 或从新进行分配)。特别是用来临时存储和处理大量信息的全局变量很是值得关注。若是你必须使用全局变量来存储大量数据,那么,请确保在使用完以后,对其赋值为 null 或者从新分配。

2:被忘记的 Timers 或者 callbacks

在 JavaScript 中使用 setInterval 很是常见。

大多数库都会提供观察者或者其它工具来处理回调函数,在他们本身的实例变为不可达时,会让回调函数也变为不可达的。对于 setInterval,下面这样的代码是很是常见的:

  1.  
    var serverData = loadData();
  2.  
    setInterval( function() {
  3.  
    var renderer = document.getElementById('renderer');
  4.  
    if(renderer) {
  5.  
    renderer.innerHTML = JSON.stringify(serverData);
  6.  
    }
  7.  
    }, 5000); //This will be executed every ~5 seconds.

这个例子阐述着 timers 可能发生的状况:计时器会引用再也不须要的节点或数据。

renderer 可能在未来会被移除,使得 interval 内的整个块都再也不被须要。可是,interval handler 由于 interval 的存活,因此没法被回收(须要中止 interval,才能回收)。若是 interval handler 没法被回收,则它的依赖也不能被回收。这意味着 serverData——可能存储了大量数据,也不能被回收。在观察者模式下,重要的是在他们再也不被须要的时候显式地去删除它们(或者让相关对象变为不可达)。

过去,特别是某些浏览器(IE6)没法管理循环引用。现在,大多数浏览器会在被观察的对象不可达时对 observer handlers 进行回收,即便 listener 没有被显式的移除。可是,明确地删除这些 observers 仍然是一个很好的作法。例如:

  1.  
    var element = document.getElementById('launch-button');
  2.  
    var counter = 0;
  3.  
    function onClick(event) {
  4.  
    counter++;
  5.  
    element.innerHtml = 'text ' + counter;
  6.  
    }
  7.  
    element.addEventListener('click', onClick);
  8.  
    // Do stuff
  9.  
    element.removeEventListener('click', onClick);
  10.  
    element.parentNode.removeChild(element);
  11.  
    // Now when element goes out of scope,
  12.  
    // both element and onClick will be collected even in old browsers // that don't handle cycles well.

现在,现代浏览器(包括 IE 和 Edge)都使用的是现代垃圾回收算法,能够检测这些循环依赖并正确的处理它们。换句话说,让一个节点不可达,能够没必要而在调用 removeEventListener。

框架和库,例如 jQuery ,在处理掉节点以前会删除 listeners (使用它们特定的 API)。这些由库的内部进了处理,确保泄漏不会发生。即便是在有问题的浏览器下运行,如。。。。IE6。

3:闭包

JavaScript 开发的一个关键方面就是闭包:一个能够访问外部(封闭)函数变量的内部函数。因为 JavaScript 运行时的实现细节,能够经过如下方式泄漏内存:

  1.  
    var theThing = null;
  2.  
    var replaceThing = function () {
  3.  
    var originalThing = theThing;
  4.  
    var unused = function () {
  5.  
    if (originalThing) // a reference to 'originalThing'
  6.  
    console.log("hi");
  7.  
    };
  8.  
    theThing = {
  9.  
    longStr: new Array(1000000).join('*'),
  10.  
    someMethod: function () {
  11.  
    console.log("message");
  12.  
    }
  13.  
    };
  14.  
    };
  15.  
    setInterval(replaceThing, 1000);

这个代码片断作了一件事:每次调用 replaceThing 时,theThing 都会得到一个新对象,它包含一个大的数组和一个新的闭包(someMethod)。同时,变量 unused 保留了一个拥有 originalThing 引用的闭包(前一次调用 theThing 赋值给了 originalThing)。已经有点混乱了吗?重要的是,一旦一个做用域被建立为闭包,那么它的父做用域将被共享。

在这个例子中,建立闭包 someMethod 的做用域是于 unused 共享的。unused 拥有 originalThing 的引用。尽管 unused 历来都没有使用,可是 someMethod 可以经过 theThing 在 replaceThing 以外的做用域使用(例如全局范围)。而且因为 someMethod 和 unused 共享 闭包范围,unused 的引用将强制保持 originalThing 处于活动状态(两个闭包之间共享整个做用域)。这样防止了垃圾回收。
当这段代码重复执行时,能够观察到内存使用量的稳定增加。当 GC 运行时,也没有变小。实质上,引擎建立了一个闭包的连接列表(root 就是变量 theThing),而且这些闭包的做用域中每个都有对大数组的间接引用,致使了至关大的内存泄漏,以下图:

image

这个问题由 Meteor 团队发现的,他们有一篇伟大的文章,详细描述了这个问题。

4:DOM 引用

有时候,在数据结构中存储 DOM 结构是有用的。假设要快速更新表中的几行内容。将每行 DOM 的引用存储在字典或数组中多是有意义的。当这种状况发生时,就会保留同一 DOM 元素的两份引用:一个在 DOM 树种,另外一个在字典中。若是未来某个时候你决定要删除这些行,则须要让两个引用都不可达。

  1.  
    var elements = {
  2.  
    button: document.getElementById('button'),
  3.  
    image: document.getElementById('image')
  4.  
    };
  5.  
    function doStuff() {
  6.  
    elements.image.src = 'http://example.com/image_name.png';
  7.  
    }
  8.  
    function removeImage() {
  9.  
    // The image is a direct child of the body element.
  10.  
    document.body.removeChild(document.getElementById('image'));
  11.  
    // At this point, we still have a reference to #button in the
  12.  
    //global elements object. In other words, the button element is
  13.  
    //still in memory and cannot be collected by the GC.
  14.  
    }

还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,能够假设 GC 将收集除了该单元格以外全部的内容。实际上,这不会发生的:该单元格是该 table 的子节点,而且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会致使整个表都保留在内存中的。保留 DOM 元素的引用时,须要仔细考虑。

相关文章
相关标签/搜索