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

随着如今的编程语言功能愈来愈成熟、复杂,内存管理也容易被你们忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便你们在使用JavaScript编码时,更好的应对内存泄漏带来的问题。javascript

概述

像C语言这样的编程语言,具备简单的内存管理功能函数,例如malloc( )和free( )。开发人员可使用这些功能函数来显式地分配和释放系统的内存。前端

当建立对象和字符串等时,JavaScript就会分配内存,并在再也不使用时自动释放内存,这种机制被称为垃圾收集。这种释放资源看似是“自动”的,但本质是混淆的,这也给JavaScript(以及其余高级语言)的开发人员产生了能够不关心内存管理的错误印象。其实这是一个大错误。java

即便使用高级语言,开发人员也应该理解内存管理的知识。有时自动内存管理也会存在问题(例如垃圾收集器中的错误或实施限制等),开发人员必须了解这些问题才能正确地进行处理。算法

内存生命周期

不管你使用的是什么编程语言,内存生命周期几乎都是同样的:express

clipboard.png

如下是对内存生命周期中每一个步骤发生的状况的概述:编程

  • 分配内存  - 内存由操做系统分配,容许程序使用它。在简单的编程语言中,这个过程是开发人员应该处理的一个显式操做。然而,在高级编程语言中,系统会帮助你完成这个操做。
  • 内存使用 -  这是程序使用以前申请内存的时间段,你的代码会经过使用分配的变量来对内存进行读取和写入操做。
  • 释放内存  - 对于再也不须要的内存进行释放的操做,以便确保其变成空闲状态而且能够被再次使用。与分配内存操做同样,这个操做在简单的编程语言中是须要显示操做的。

什么是内存?

在硬件层面上,计算机的内存由大量的触发器组成的。每一个触发器包含一些晶体管,并可以存储一位数据。单独的触发器能够经过惟一的标识符来寻址,因此咱们能够读取和覆盖它们。所以,从概念上讲,咱们能够把整个计算机内存看做是咱们能够读写的一大块空间。数组

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

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

编译器和操做系统一块儿工做,来处理大部分的内存管理,可是咱们须要了解从本质上发生了什么。缓存

编译代码时,编译器会检查原始数据类型,并提早计算它们须要多少内存,而后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间被称为堆栈空间,随着函数的调用,内存会被添加到现有的内存之上。当终止时,空间以LIFO(后进先出)顺序被移除。例如以下声明:session

int n; // 4个字节
int x [4]; // 4个元素的数组,每个占4个字节
double m; // 8个字节

编译器插入与操做系统进行交互的代码,以便在堆栈中请求所需的字节数来存储变量。

在上面的例子中,编译器知道每一个变量的确切内存地址。实际上,每当咱们写入这个变量n,它就会在内部翻译成“内存地址4127963”。

注意,若是咱们试图访问x[4],咱们将访问与m关联的数据。这是由于咱们正在访问数组中不存在的元素 - 它比数组中最后一个数据实际分配的元素多了4个字节x[3],而且可能最终读取(或覆盖)了一些m比特。这对其他部分会产生不利的后果。

clipboard.png

当函数调用其它函数时,每一个函数被调用时都会获得本身的堆栈块。它会保留全部的局部变量和一个程序计数器,还会记录执行的地方。当功能完成时,其内存块会被释放,能够再次用于其它目的。

动态分配

如若咱们不知道编译时,变量须要的内存数量时,事情就会变得复杂。假设咱们想要作以下事项:

int n = readInput(); //读取用户的输入
...
//用“n”个元素建立一个数组

在编译时,编译器不知道数组须要多少内存,由于它是由用户提供的输入值决定的。

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

clipboard.png

在JavaScript中分配内存

如今来解释如何在JavaScript中分配内存。

JavaScript使得开发人员免于处理内存分配的工做。

var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string

var o = {
  a: 1,
  b: null
}; // allocates memory for an object and its contained values

var a = [1, null, 'str'];  // (like object) allocates memory for the
                           // array and its contained values

function f(a) {
  return a + 3;
} // allocates a function (which is a callable object)

// function expressions also allocate an object
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

一些函数调用也会致使对象分配:

var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element

方法能够分配新的值或对象:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable, 
// JavaScript may decide to not allocate memory, 
// but just store the [0, 3] range.

var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2); 
// new array with 4 elements being
// the concatenation of a1 and a2 elements

在JavaScript中使用内存

基本上在JavaScript中使用分配的内存,意味着在其中读写。

这能够经过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。

当内存再也不须要时进行释放

大部份内存泄漏问题都是在这个阶段产生的,这个阶段最难的问题就是肯定什么时候再也不须要已分配的内存。它一般须要开发人员肯定程序中的哪一个部分再也不须要这些内存,并将其释放。

高级语言嵌入了一个名为垃圾收集器的功能,其工做是跟踪内存分配和使用状况,以便在再也不须要分配内存的状况下自动释放内存。

不幸的是,这个过程没法作到那么准确,由于像某些内存再也不须要的问题是不能由算法来解决的。

大多数垃圾收集器经过收集不能被访问的内存来工做,例如指向它的变量超出范围的这种状况。然而,这种方式只能收集内存空间的近似值,由于在内存的某些位置可能仍然有指向它的变量,但它却不会被再次访问。

因为肯定一些内存是否“再也不须要”,是不可断定的,因此垃圾收集机制就有必定的局限性。下面将解释主要垃圾收集算法及其局限性的概念。

内存引用

垃圾收集算法所依赖的主要概念之一就是内存引用。

在内存管理状况下,若是一个对象访问变量(能够是隐含的或显式的),则称该对象引用另外一个对象。例如,JavaScript对象具备对其原对象(隐式引用)及其属性值(显式引用)的引用。

在这种状况下,“对象”的概念扩展到比普通JavaScript对象更普遍的范围,而且还包含函数范围。

引用计数垃圾收集

这是最简单的垃圾收集算法。若是有零个引用指向它,则该对象会被认为是“垃圾收集” 。

看看下面的代码:

var o1 = {
  o2: {
    x: 1
  }
};

// 2 objects are created. 
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected


var o3 = o1; // the 'o3' variable is the second thing that 
            // has a reference to the object pointed by 'o1'.

                                                       
o1 = 1;      // now, the object that was originally in 'o1' has a         
            // single reference, embodied by the 'o3' variable

var o4 = o3.o2; // reference to 'o2' property of the object.
                // This object has now 2 references: one as
                // a property. 
                // The other as the 'o4' variable

o3 = '374'; // The object that was originally in 'o1' has now zero
            // references to it. 
            // It can be garbage-collected.
            // However, what was its 'o2' property is still
            // referenced by the 'o4' variable, so it cannot be
            // freed.

o4 = null; // what was the 'o2' property of the object originally in
           // 'o1' has zero references to it. 
           // It can be garbage collected.

周期引发问题

在周期方面有一个限制。例以下面的例子,建立两个对象并相互引用,这样会建立一个循环引用。在函数调用以后,它们将超出范围,因此它们其实是无用的,能够被释放。然而,引用计数算法认为,因为两个对象中的每个都被引用至少一次,因此二者都不能被垃圾收集机制收回。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}

f( );

标记和扫描算法

为了决定是否须要对象,标记和扫描算法会肯定对象是不是活动的。

标记和扫描算法通过如下3个步骤:

roots:一般,root是代码中引用的全局变量。例如,在JavaScript中,能够充当root的全局变量是“窗口”对象。Node.js中的相同对象称为“全局”。全部root的完整列表由垃圾收集器构建。
而后算法会检查全部root和他们的子对象而且标记它们是活动的(即它们不是垃圾)。任何root不能达到的,将被标记为垃圾。
最后,垃圾回收器释放全部未标记为活动的内存块,并将该内存返回给操做系统。

这个算法比引用计数垃圾收集算法更好。JavaScript垃圾收集(代码/增量/并发/并行垃圾收集)领域中所作的全部改进都是对这种标记和扫描算法的实现改进,但不是对垃圾收集算法自己的改进。

周期再也不是问题了

在上面的相互引用例子中,在函数调用返回以后,两个对象再也不被全局对象可访问的对象引用。所以,它们将被垃圾收集器发现,从而进行收回。

clipboard.png

即便在对象之间有引用,它们也不能从root目录中访问,从而会被认为是垃圾而收集。

抵制垃圾收集器的直观行为

尽管垃圾收集器使用起来很方便,但它们也有本身的一套标准,其中之一是非决定论。换句话说,垃圾收集是不可预测的。你不能真正知道何时进行收集,这意味着在某些状况下,程序会使用更多的内存,虽然这是实际须要的。在其它状况下,在特别敏感的应用程序中,短暂暂停是极可能出现的。尽管非肯定性意味着不能肯定什么时候进行集合,但大多数垃圾收集实现了共享在分配期间进行收集的通用模式。若是没有执行分配,大多数垃圾收集会保持空闲状态。如如下状况:

  1. 大量的分配被执行。
  2. 大多数这些元素(或全部这些元素)被标记为没法访问(假设咱们将一个引用指向再也不须要的缓存)。
  3. 没有进一步的分配执行。

在这种状况下,大多数垃圾收集不会作出任何的收集工做。换句话说,即便有不可用的引用须要收集,可是收集器不会进行收集。虽然这并非严格的泄漏,但仍会致使内存使用率高于平时。

什么是内存泄漏?

内存泄漏是应用程序使用过的内存片断,在再也不须要时,不能返回到操做系统或可用内存池中的状况。

编程语言有各自不一样的内存管理方式。可是是否使用某一段内存,其实是一个不可断定的问题。换句话说,只有开发人员明确的知道是否须要将一块内存返回给操做系统。

四种常见的JavaScript内存泄漏

1:全局变量

JavaScript以一种有趣的方式来处理未声明的变量:当引用未声明的变量时,会在全局对象中建立一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg) {
    bar = "some text";
}

至关于:

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

bar只是foo函数中引用一个变量。若是你不使用var声明,将会建立一个多余的全局变量。在上述状况下,不会形成很大的问题。可是,如如果下面的这种状况。

你也可能不当心建立一个全局变量this:

function foo() {
    this.var1 = "potential accidental global";
}

// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo( );

你能够经过在JavaScript文件的开始处添加‘use strict’;来避免这中错误,这种方式将开启严格的解析JavaScript模式,从而防止意外建立全局变量。

意外的全局变量固然是一个问题。更多的时候,你的代码会受到显式的全局变量的影响,而这些全局变量在垃圾收集器中是没法收集的。须要特别注意用于临时存储和处理大量信息的全局变量。若是必须使用全局变量来存储数据,那么确保将其分配为空值,或者在完成后从新分配。

2:被遗忘的定时器或回调

下面列举setInterval的例子,这也是常常在JavaScript中使用。

对于提供监视的库和其它接受回调的工具,一般在确保全部回调的引用在其实例没法访问时,会变成没法访问的状态。可是下面的代码倒是一个例外:

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

上面的代码片断显示了使用引用节点或再也不须要的数据的定时器的结果。

该renderer对象可能会在某些时候被替换或删除,这会使interval处理程序封装的块变得冗余。若是发生这种状况,那么处理程序及其依赖项都不会被收集,由于interval须要先中止。这一切都归结为存储和处理负载数据的serverData不会被收集的缘由。

当使用监视器时,你须要确保作了一个明确的调用来删除它们。

幸运的是,大多数现代浏览器都会为你作这件事:即便你忘记删除监听器,当被监测对象变得没法访问,它们就会自动收集监测处理器。这是过去的一些浏览器没法处理的状况(例如旧的IE6)。

看下面的例子:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);

// Do stuff

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.

因为现代浏览器支持垃圾回收机制,因此当某个节点变的不能访问时,你再也不须要调用removeEventListener,由于垃圾回收机制会恰当的处理这些节点。

若是你正在使用jQueryAPI(其余库和框架也支持这一点),那么也能够在节点不用以前删除监听器。即便应用程序在较旧的浏览器版本下运行,库也会确保没有内存泄漏。

3:闭包

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

var theThing = null;

var replaceThing = function(){

  var originalThing = theThing; 
  var unused = function(){ 
    if(originalThing)//对'originalThing'的引用
      console.log(“hi”); 
  };

  theThing = { 
    longStr:new Array(1000000).join('*'),
    someMethod:function(){ 
      console.log(“message”); 
    } 
  }; 
};

setInterval(replaceThing,1000);

一旦replaceThing被调用,theThing会获取由一个大数组和一个新的闭包(someMethod)组成的新对象。然而,originalThing会被unused变量所持有的闭包所引用(这是theThing从之前的调用变量replaceThing)。须要记住的是,一旦在同一父做用域中为闭包建立了闭包的做用域,做用域就被共享了。

在这种状况下,闭包建立的范围会将someMethod共享给unused。然而,unused有一个originalThing引用。即便unused从未使用过,someMethod 也能够经过theThing在整个范围以外使用replaceThing。并且someMethod经过unused共享了闭包范围,unused必须引用originalThing以便使其它保持活跃(两封闭之间的整个共享范围)。这就阻止了它被收集。

全部这些均可能致使至关大的内存泄漏。当上面的代码片断一遍又一遍地运行时,你会看到内存使用率的不断上升。当垃圾收集器运行时,其内存大小不会缩小。这种状况会建立一个闭包的链表,而且每一个闭包范围都带有对大数组的间接引用。

4:超出DOM引用

在某些状况下,开发人员会在数据结构中存储DOM节点,例如你想快速更新表格中的几行内容的状况。若是在字典或数组中存储对每一个DOM行的引用,则会有两个对同一个DOM元素的引用:一个在DOM树中,另外一个在字典中。若是你再也不须要这些行,则须要使两个引用都没法访问。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};

function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}

function removeImage() {
    // The image is a direct child of the body element.
    document.body.removeChild(document.getElementById('image'));
    // At this point, we still have a reference to #button in the
    //global elements object. In other words, the button element is
    //still in memory and cannot be collected by the GC.
}

在涉及DOM树内的内部节点或叶节点时,还有一个额外的因素须要考虑。若是你在代码中保留对表格单元格(标签)的引用,并决定从DOM中删除该表格,还须要保留对该特定单元格的引用,则可能会出现严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元以外的全部东西,但状况并不是如此。因为单元格是表格的一个子节点,而且子节点保留着对父节点的引用,因此对表格单元格的这种引用,会将整个表格保存在内存中。

JavaScript 开发工具推荐

SpreadJS 纯前端表格控件是基于 HTML5 的 JavaScript 电子表格和网格功能控件,提供了完备的公式引擎、排序、过滤、输入控件、数据可视化、Excel 导入/导出等功能,适用于 .NET、Java 和移动端等各平台在线编辑类 Excel 功能的表格程序开发。

总结

以上内容是对JavaScript内存管理机制的讲解,以及常见的四种内存泄漏的分析。但愿对JavaScript的编程人员有所帮助。

原文连接:https://blog.sessionstack.com...

转载请注明出自:葡萄城控件

关于葡萄城

葡萄城成立于1980年,是全球最大的控件提供商,世界领先的企业应用定制工具、企业报表和商业智能解决方案提供商,为超过75%的全球财富500强企业提供服务。葡萄城于1988年在中国设立研发中心,在全球化产品的研发过程当中,不断适应中国市场的本地需求,并为软件企业和各行业的信息化提供优秀的软件工具和咨询服务。

相关文章
相关标签/搜索