该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。第二篇文章深刻剖析了Google的V8 JavaScript引擎,并提供了关于如何编写更好的JavaScript代码的一些提示。javascript
在第三篇文章中,咱们将讨论另外一个愈来愈被开发人员忽视的关键主题,由于平常使用的编程语言(内存管理)愈来愈成熟和复杂。咱们还会提供一些关于如何处理内存泄漏的技巧。前端
相似与C这种编程语言,提供了从底层来管理内存的方法,好比malloc()和free()。开发人员能够经过它们,来处理操做系统的分配内存,或释放内存到操做系统中。
在JavaScript当中,当对象或字符串等被建立时,JavaScript会申请和分配内存;当对象或字符再也不被使用时,它们就会被自动释放,这个过程被称为垃圾处理。正是这种自动看似自动回收的认识让JavaScript开发者误觉得他们不用关心内存管理,这是一个很大的错误
即便使用高级语言,开发者也应该理解内存管理(即使是基础),有时自动内存管理也会有一些问题(例如bug或者垃圾回收实现的局限性等等),因此开发者必需要明白它们,才可以妥善的处理。java
不管你使用什么语言,内存的生命周期大致是相同的: 算法
有关调用堆栈和内存堆的概念的概述,您能够阅读本系列第一篇文章。编程
在开始讨论JavaScript的内存以前,咱们先短暂的讨论一下相关概念和内存是怎么工做的。
在硬件层面之上,电脑的内存是由大量的触发器,每一个触发器都包含一些晶体管而且可以存储一个bit。单个触发器可经过惟一标识符进行寻址,这样就能够读取并覆盖它们。所以,从概念上讲,咱们能够将整个计算机内存看做是咱们能够读写的bit数组。
从人类角度来讲,咱们不擅长用bit来完成咱们现实中思想和算法,咱们把它们组织成更大的部分,它们一块儿能够用来表示数字。 8位(比特位)称为1个字节(byte)。除字节外,还有单词(word)(有时是16,有时是32位)。数组
不少东西都存储在这个内存中:浏览器
编译代码时,编译器能够预先检查原始数据类型并提早计算它们须要多少内存。而后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间称为堆栈空间,由于随着函数的调用,它们的内存将被添加到现有内存之上。当它们终止时,它们以LIFO(后进先出)顺序被移除。例如,请考虑如下声明:缓存
int n; // 4字节
int x [4]; // 4个元素的数组,每一个4个字节
double m; // 8个字节
复制代码
编译器能够当即看到代码须要:
4 + 4×4 + 8 = 28个字节。session
这就是它如何处理整数和双精度的当前大小。大约20年前,整数一般是2个字节,而且是双4字节。您的代码不该该依赖于此时基本数据类型的大小。闭包
编译器将插入与操做系统进行交互的代码,以在堆栈中请求必要的字节数,以便存储变量。
在上面的例子中,编译器知道每一个变量的确切内存地址。事实上,只要咱们写入变量n,就会在内部翻译成相似“内存地址4127963”的内容。
注意,若是咱们试图在这里访问x[4]
,咱们将访问与m关联的数据。这是由于咱们正在访问数组中不存在的元素 - 它比数组中最后一个实际分配的元素x[3]
更远了4个字节,而且可能最终读取(或覆盖)m中的一些位。这儿就会有bug了。
不幸的是,当咱们在编译时有时不知道变量须要多少内存时,假设咱们想要作以下的事情:
int n=readInput();//用户的输入
...
//常见一个长度为n的数组
复制代码
在编译时,编译器不知道数组须要多少内存,由于它由用户提供的值决定。
所以,它不能为堆栈上的变量分配空间。
相反,咱们的程序须要在运行时明确要求操做系统提供适当的空间。
该内存是从堆空间分配的。 下表总结了静态和动态内存分配之间的区别:
如今咱们将解释第一步(分配内存),以及它如何在JavaScript中工做。 JavaScript减轻了开发人员处理内存分配的责任-JavaScript自身声明的时候就分配内存,而后赋值。
var n = 374; // 为数字分配内存
var s = 'sessionstack'; // 为字符串分配内存
var o = {
a: 1,
b: null
}; // 为对象和它的值分配内存
var a = [1, null, 'str']; // (相似对象) 为数组和它的值分配内存
function f(a) {
return a + 3;
} // 为函数分配内存 (这是一个可调用的方法对象)
// 函数表达式也会分配内存
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
复制代码
一些函数调用也会致使对象分配:
var d = new Date(); // 为日期对象分配内存
var e = document.createElement('div'); // 为DOM元素分配内存
复制代码
方法能够分配新的值或对象:
var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不可改变的,
// JavaScript 可能决定不分配内存,
// 仅仅只保存 [0, 3] 这个范围.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// 新的对象有四个元素,它是由a1和a2链接而成
复制代码
基本上在JavaScript中使用分配的内存意味着读取和写入。
这能够经过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。
大部份内存管理问题都是在这个阶段出现的。
肯定什么时候再也不须要使用分配的内存是最困难的。它一般须要开发人员肯定程序中的哪一个地方再也不须要这些内存,并将其释放。
高级语言嵌入了一个名为垃圾收集器的软件,其工做是跟踪内存分配和使用状况,以便找到什么时候再也不须要分配的内存,在这种状况下,它会自动释放它。
不幸的是,这个过程是一个大概,由于知道是否须要某些内存的通常问题是不可断定的(不能由算法解决)。
大多数垃圾收集器经过收集不能再访问的内存来工做,例如,指向它的全部变量都超出了范围。然而,这只能够收集的一组内存空间的近似值,由于在任什么时候候内存位置可能仍然有一个指向它的变量,但它将不会再被访问。
因为发现某些内存是否“再也不须要”的事实是不可断定的,因此垃圾收集实现了对通常问题的解决方案的限制。本节将解释理解主要垃圾收集算法及其局限性的必要概念。
垃圾收集算法所依赖的主要概念是参考之一。
在内存管理的上下文中,若是一个对象能够访问后者(能够是隐式或显式的),则称该对象引用另外一个对象。例如,JavaScript对象具备对其原型(隐式引用)及其属性值(显式引用)的引用。
在这种状况下,“对象”的概念扩展到比常规JavaScript对象更普遍的范围,而且还包含函数范围(或全局词法范围)。
词法范围定义了如何在嵌套函数中解析变量名称:即便父函数已返回,内部函数也包含父函数的做用域。
这是最简单的垃圾收集算法。若是指向它引用数时零,则该对象被视为“垃圾可收集的” 。
看下下面的代码:
var o1 = {
o2: {
x: 1
}
};
// 两个对象被建立.
// 'o2'做为'o1'的属性被引用.
// 不可以被当作可回收的
var o3 = o1; //'o3'是第二个有引用的,它被指向了'o1' .
o1 = 1; //如今,最初在'o1'中的对象有一个引用,由'o3'变量体现出来
var o4 = o3.o2; // 引用到'o2'做为属性的对象.
// 这个对象如今有两个引用:一个做为属性.
// 另外一个变成了 'o4' 的值
o3 = '374'; // 如今这个最初的'o1'变成了零引用了,他能够被垃圾回收
//然而,'o2'变量仍然做为'o4'变量的属性,他不能被释放
o4 = null; // 如今对于'o2',没有地方应用它了,他能够被垃圾回收
复制代码
在循环引用方面存在限制。在如下示例中,建立了两个对象并相互引用,从而建立了一个循环。在函数调用以后它们将超出范围,所以它们其实是无用的而且能够被释放。可是,引用计数算法认为,因为两个对象中的每个至少被引用一次,所以二者都不能被垃圾收集。
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 引用 o2
o2.p = o1; // o2 引用 o1. 造成了循环.
}
f();
复制代码
为了肯定是否须要一个对象,该算法肯定对象是否能够得到。
标记和扫描算法经过如下3个步骤:
截至2012年,全部现代浏览器都提供了标记 - 清除垃圾收集器。在过去几年中,在JavaScript垃圾收集(生成/增量/并发/并行垃圾收集)领域所作的全部改进都是该算法的实现改进(标记和清除),不只不是对垃圾收集算法自己的改进,也不是判断一个对象是否可及做为目标。
在本文中,您能够更详细地阅读跟踪垃圾收集,其中还包括标记和清除及其优化。
在上面的第一个示例中,在函数调用返回以后,两个对象再也不被从全局对象可到达的内容引用。所以,垃圾收集器将没法访问它们。
虽然垃圾收集器很方便,但它们有本身的权衡取舍。其中之一是非决定论。换句话说,GC是不可预测的。您没法肯定什么时候会执行收集。这意味着在某些状况下,程序会使用更多实际须要的内存。在其余状况下,在特别敏感的应用中,短暂停顿可能会很明显。尽管非肯定性意味着没法肯定什么时候执行集合,但大多数GC的实现都是在分配期间执行集合过程这种常见模式。若是没有执行分配,则大多数GC保持空闲。请考虑如下情形:
就像内存所暗示的那样,内存泄漏是应用程序过去使用但再也不须要但还没有返回操做系统或可用内存池的内存块。
JavaScript以一种有趣的方式处理未声明的变量:当引用未声明的变量时,会在全局对象中建立一个新变量。 在浏览器中,全局对象将是window,这意味着:
function foo(arg) {
bar = "some text";
}
复制代码
等同于
function foo(arg) {
window.bar = "some text";
}
复制代码
假设bar
的目的是仅引用foo函数中的变量。可是,若是您不使用var来声明它,将会建立一个冗余的全局变量。在上述状况下,这不会形成太大的伤害。
尽管如此,你必定能够想象一个更具破坏性的场景。
你也能够用这个意外地建立一个全局变量:
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模式,从而防止意外建立全局变量。
意外的全局变量固然是一个问题,然而,更多的时候,你的代码会受到显式定义的全局变量的影响,这些变量不能被垃圾收集器回收。须要特别注意用于临时存储和处理大量信息的全局变量。若是你必须使用全局变量来存储数据,用完以后必定要把它赋值为null或者在完成以后从新赋值。
以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须要先中止(请记住,它仍然处于活动状态)。这一切归结为serverData确实存储和处理负载数据的事实也不会被收集。
当使用observers时,你须要确保你作了一个明确的调用,在完成它们以后将其删除(再也不须要观察者,不然对象将没法访问)。
幸运的是,大多数现代浏览器都会为您完成这项工做:即便您忘记删除侦听器,一旦观察到的对象变得没法访问,他们会自动收集观察者处理程序。在过去,一些浏览器没法处理这些状况(旧版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。
若是您利用jQuery API(其余库和框架也支持这一点),您也能够在节点过期以前删除侦听器。 即便应用程序在较旧的浏览器版本下运行,该库也会确保没有内存泄漏。
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变量保持。须要记住的是,当一个闭包的做用域被建立,同属父范围内的闭包的做用域会被共享。
在这种状况下,闭包someMethod建立的做用域将与闭包unused的做用域共享。unused引用了originalThing,尽管代码中unused从未被调用过,可是咱们仍是能够在replaceThing函数外经过theThing来调用someMethod。因为someMethod与unused的闭包做用域共享,闭包unused的引用了originalThing,强制它保持活动状态(两个闭包之间的共享做用域)。这阻止了它被垃圾回收。
在上面的例子中,闭包someMethod建立的做用域与闭包unused做用域的共享,而unused的引用originalThing。尽管闭包unused从未被使用,someMethod仍是能够经过theThing,从replaceThing范围外被调用。事实上,闭包unused引用了originalThing要求它保持活动,由于someMethod与unused的做用域共享。
闭包会保留一个指向其做用域的指针,做用域就是闭包父函数,因此闭包unused和someMethod都会有一个指针指向replaceThing函数,这也是为何闭包能够访问外部函数的变量。因为闭包unused引用了originalThing变量,这使得originalThing变量存在于lexical environment,replaceThing函数里面定义的全部的闭包都会有一个对originalThing的引用,因此闭包someMethod天然会保持一个对originalThing的引用,因此就算theThing替换成其它值,它的上一次值不会被回收。
全部这些均可能致使至关大的内存泄漏。当上面的代码片断一遍又一遍地运行时,您可能会发现内存使用量激增。当垃圾收集器运行时,其大小不会缩小。建立了一个闭包的链表(在这种状况下,它的根就是theThing变量),而且每一个闭包范围都会间接引用大数组。
有些状况下开发者会保存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() {
// image元素是body的子元素
document.body.removeChild(document.getElementById('image'));
// 这时咱们还有一个对 #image 的引用,这个引用在elements对象中
// 换句话说,image元素还在内存中,不能被GC回收
}
复制代码
涉及DOM树内的内部节点或叶节点时,还有一个额外须要考虑的问题。若是在代码中保留对表格单元格(一个<td>
标签)的引用,并决定从DOM中删除该表格并保留对该特定单元格的引用,则能够预期会出现严重的内存泄漏。你可能会认为垃圾回收器会释放该这个单元格外的全部内容。然而,状况并不是如此。因为单元格是表格的子节点,而且子节点保持对父节点的引用,所以对表格单元格的这种单引用将使整个表格保留在内存中,不能被GC回收。
后续文档翻译会陆续跟进!!
欢迎关注玄说前端公众号,后续将推出系列文章《一个大型图形化应用0到1的过程》,此帐户也将同步更新