JavaScript 是如何工做的:内存管理 + 如何处理四种常见的内存泄漏

JavaScript 是如何工做的:内存管理 + 如何处理四种常见的内存泄漏

原文 - How JavaScript works: memory management + how to handle 4 common memory leaksjavascript

原文做者 - Alexander Zlatkovhtml

原文地址 - blog.sessionstack.com/how-javascr…java

译者 - yanleenode

译文地址 - yanlee26.github.io/2018/02/01/…git

知乎专栏 - zhuanlan.zhihu.com/p/33483627 掘金专栏 - juejin.im/post/5a725c…github

几个星期前,咱们开始了一系列旨在深刻研究JavaScript及其实际工做原理的系列文章:咱们认为经过了解JavaScript的构建块以及它们如何一块儿玩,您将可以编写更好的代码和应用程​​序。算法

本系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述第二偏文章仔细研究谷歌的V8 JavaScript引擎的内部零件,也提供了有关如何写出更好的JavaScript代码的一些提示。express

在这个第三篇文章中,咱们将讨论另外一个愈来愈被开发人员忽视的关键主题,由于平常使用的编程语言(内存管理)愈来愈成熟和复杂。咱们也将提供关于如何处理JavaScript中的内存泄漏,咱们在一些技巧SessionStack中遵循咱们须要确保SessionStack不会形成内存泄漏,或不增长的Web应用程序,咱们正在整合的内存消耗。编程

概观

像C这样的语言具备低级的内存管理原语,好比malloc()和free()。开发人员使用这些原语来显式分配和释放操做系统的内存。数组

同时,当事物(对象,字符串等)被建立时,JavaScript分配内存,并在再也不使用时自动释放内存,称为垃圾收集。这种释放资源的看似“自动化”特性是混淆的一个缘由,给JavaScript(和其余高级语言)的开发人员带来了他们能够选择不关心内存管理的错误印象。这是一个大错误。

即便使用高级语言,开发人员也应该理解内存管理(至少是基本的)。有时,自动内存管理存在问题(例如垃圾收集器中的错误或实施限制等),开发人员必须了解这些问题才能正确处理这些问题(或者找到适当的解决方法,而且具备最小的权衡和代码债务)。

内存生命周期

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

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96c6619394?w=1024&h=768&f=png&s=16698

如下是对循环中每一个步骤发生的状况的概述:

  • 分配内存  - 内存由操做系统分配,容许程序使用它。在低级语言中(例如C),这是一个做为开发人员应该处理的显式操做。然而,在高级语言中,这是为你照顾的。
  • 使用内存 - 这是您的程序实际上使用之前分配的内存的时间。读取和写入操做正在您的代码中使用分配的变量。
  • 释放内存  - 如今是释放你不须要的整个内存的时间,以便它能够变成空闲的而且能够再次使用。与分配内存操做同样,这个操做在低级语言中是明确的。

有关调用堆栈和内存堆的概念的快速概述,您能够阅读咱们关于主题的第一篇文章

什么是内存?

在直接跳到JavaScript中的内存以前,咱们将简要地讨论一下内存的概况以及它是如何工做的。

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

既然做为人类,咱们并不善于把全部的思想和算术都作成一点点,咱们把它们组织成更大的群体,它们能够一块儿用来表示数字。8位称为1个字节。除字节外,还有单词(有时是16,有时是32位)。

不少东西都存储在这个内存中:

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

编译器和操做系统一块儿工做,为您处理大部分的内存管理,可是咱们建议您看看底下发生了什么。

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

int n; // 4个字节
int x [4]; // 4个元素的数组,每4个字节
双m; // 8个字节
复制代码

编译器能够当即看到代码须要
4 + 4×4 + 8 = 28个字节。

这就是它与目前的整数和双打的尺寸。大约20年前,整数一般是2个字节,双4字节。您的代码不该该依赖于此刻基本数据类型的大小。

编译器将插入与操做系统交互的代码,以便为堆栈中的变量存储所需的字节数。

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

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

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96c674afa3?w=1024&h=353&f=png&s=10304

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

动态分配

不幸的是,当咱们不知道编译时变量须要多少内存时,事情并不那么容易。假设咱们想要作以下的事情:

int n = readInput(); //从用户读取输入
//用“n”个元素建立一个数组
复制代码

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

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

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96c6e8ff49?w=1024&h=431&f=png&s=71479
静态和动态分配的内存之间的差别

为了充分理解动态内存分配是如何工做的,咱们须要在指针上花费更多的时间,这可能与本文主题偏离太多。若是您有兴趣了解更多信息,请在评论中告诉咱们,咱们能够在之后的文章中详细介绍指针。

在JavaScript中(内存)分配

如今咱们将解释第一步(分配内存)如何在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中使用分配的内存,意味着在其中读写。

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

当内存再也不须要时释放

大部份内存管理问题都是在这个阶段。

这里最困难的任务是肯定什么时候再也不须要分配的内存。它一般须要开发人员肯定程序中的哪一个部分再也不须要这些内存,并将其释放。

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

不幸的是,这个过程是一个近似值,由于知道是否须要某些内存的通常问题是不可断定的(不能由算法来解决)。

大多数垃圾收集器经过收集不能被访问的内存来工做,例如指向它的全部变量超出范围。然而,这是能够收集的一组内存空间的近似值,由于在任什么时候候内存位置可能仍然有一个指向它的变量,但它将不会被再次访问。

垃圾收集

因为发现一些内存是否“再也不须要”的事实是不可断定的,因此垃圾收集实现了对通常问题的解决方案的限制。本节将解释理解主要垃圾收集算法及其局限性的必要概念。

内存引用

垃圾收集算法所依赖的主要概念是引用(reference)之一。

在内存管理的状况下,若是一个对象访问后者(能够是隐含的或显式的),则称该对象引用另外一个对象。例如,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();
复制代码

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96bfe93807?w=386&h=209&f=png&s=10259

标记和扫描算法

为了肯定是否须要对象,此算法肯定对象是否可达。

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

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

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96c6b097aa?w=972&h=514&f=gif&s=268959

标记和扫描算法的可视化

这个算法比前一个算法更好,由于“一个对象有零引用”致使这个对象没法访问。正如咱们已经看到周期同样,状况正好相反。

截至2012年,全部现代浏览器都发布了标记式的垃圾回收器。JavaScript垃圾收集(代码/增量/并发/并行垃圾收集)领域中所作的全部改进都是对这种算法(标记和扫描)的实现改进,但不是对垃圾收集算法自己的改进,也不是它的目标是决定一个对象是否可达。

本文中,您能够详细阅读有关跟踪垃圾回收的更详细信息,这些垃圾回收也涵盖了标记和扫描以及其优化。

循环再也不是问题了

在上面的第一个例子中,在函数调用返回以后,两个对象再也不被全局对象可访问的东西引用。所以,它们将被垃圾收集器发现没法访问。

https://user-gold-cdn.xitu.io/2018/2/1/1614eb96c7179f06?w=1024&h=768&f=png&s=24898

即便在对象之间有引用,它们也不能从根目录访问。

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

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

  • 大量的分配被执行。
  • 大多数这些元素(或全部这些元素)被标记为没法访问(假设咱们将一个引用指向咱们再也不须要的缓存)。
  • 没有进一步的分配执行。 在这种状况下,大多数GC不会运行任何进一步的收集通行证。换句话说,即便有不可用的引用可用于收集,这些收集器不会声明。这些并非严格的泄漏,但仍会致使内存使用率高于平时。

什么是内存泄漏?

就像内存建议同样,内存泄漏是应用程序过去使用的内存片断,但再也不须要,但还没有返回到操做系统或可用内存池。

https://user-gold-cdn.xitu.io/2018/2/1/1614eb979b71d731?w=450&h=437&f=jpeg&s=41174

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

某些编程语言提供了帮助开发人员执行此操做 其余人则但愿开发人员可以彻底清楚一段内存什么时候未被使用。维基百科有关手动和自动内存管理的好文章。

四种常见的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();
复制代码

您能够经过‘use strict’;在JavaScript文件的开始处添加以免全部这些,这将开启更严格的解析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对象可能会在某些时候被替换或删除,这会使间隔处理程序封装的块变得冗余。若是发生这种状况,那么处理程序及其依赖项都不会被收集,由于间隔须要先中止(请记住,它仍然是活动的)。这一切都归结为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强制它保持活跃(两封闭之间的整个共享范围)。这阻止了它的收集。

在上面的例子中,所述封闭建立的范围someMethod与共享unused,而unused引用originalThing。someMethod能够theThing在replaceThing范围以外使用,尽管这unused是历来没有使用的事实。事实上,未使用的引用originalThing要求它保持活跃,由于someMethod与未使用的共享封闭范围。

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

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

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中删除该表格,并保留对该特定单元格的引用,则能够预期会出现严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元以外的全部东西。但状况并不是如此。因为单元格是表格的子节点,而且子节点保持对父节点的引用,因此对表格单元格的这种单引用能够将整个表格保存在内存中。

咱们在SessionStack尝试遵循这些最佳实践,编写正确处理内存分配的代码,缘由以下: 一旦将SessionStack集成到生产Web应用程序中,它就会开始记录全部事件:全部DOM变动,用户交互,JavaScript异常,堆栈跟踪,网络请求失败,调试消息等等。
使用SessionStack,您能够在Web应用程序中重放问题,看到你的用户发生的一切。全部这些都必须在您的网络应用程序没有性能影响的状况下进行。 因为用户能够从新加载页面或导航你的应用程序,全部的观察者,拦截器,变量分配等都必须正确处理,因此它们不会致使任何内存泄漏,或者不会增长Web应用程序的内存消耗咱们正在整合。

有一个免费的计划,因此你能够试试看

https://user-gold-cdn.xitu.io/2018/2/1/1614eb9795f6cf61?w=1031&h=573&f=png&s=86009

参考

相关文章
相关标签/搜索