JavaScript 内存管理 & 垃圾回收机制

简介

低级语言,比如C,有低级的内存管理基元,像 malloc(),free()。另一方面,JavaScript 的内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动释放”。后者被称为垃圾回收。这个“自动”是混淆并给 JavaScript (和其他高级语言)开发者一个错觉:他们可以不用考虑内存管理。

JavaScript 的内存分配

变量初始化

为了不让程序员为分配费心,JavaScript 在定义变量时完成内存分配。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
var n = 123; // 给数值变量分配内存
// 为对象及其包含变量分配内存
var o = {
a: 1,
b: null
};
// 函数表达式也能分配一个对象
obj.addEventListener("click", function(){
obj.style.backgroundColor = 'blue';
}, false);

通过函数调用的内存分配

有些函数调用结果是分配对象内存:

1
2
var d = new Date();
var e = document.createElement("div");

值的使用

使用值的过程实际上是对分配内存进行读取与写入的操作,这意味着可以写入一个变量或者一个对象的属性值,甚至传递函数的参数。

当内存不再需要使用时释放

大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。

高级语言解释器嵌入了“垃圾回收器”,主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这个过程是一个近似的,因为要知道某块内存是否需要是 无法判定的 (无法被某种算法所解决)。

垃圾回收机制——GC

JavaScript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

JavaScript 垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

还是上代码说明吧:

1
2
3
4
5
6
7
8
9
function fn1() {
var obj = {name: 'yu', age: 10};
}
function fn2() {
var obj = {name: 'yu', age: 11};
return obj;
}
var a = fn1();
var b = fn2();

我们来看代码是如何执行的。首先定义了两个 function,分别叫做 fn1 和 fn2,当 fn1 被调用时,进入 fn1 的环境,会开辟一块内存存放对象,而当调用结束后,出了 fn1 的环境,那么该块内存会被 js 引擎中的垃圾回收器自动释放;在 fn2 被调用的过程中,返回的对象被全局变量 b 所指向,所以该块内存并不会被释放。

这里问题就出现了:到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。引用计数不太常用,标记清除较为常用。

标记清除

js 中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个而变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

引用计数

这是最简单的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到了一个严重的问题:循环引用。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。

1
2
3
4
5
6
7
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();

以上代码a和b的引用次数都是2,fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄漏。在IE7与IE8上,内存直线上升。
最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样

1
2
myObject.element = null;
element.o = null;

内存管理

1、什么时候触发垃圾回收?
垃圾回收周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6 的垃圾回收是根据内存分配量运行的,当环境中存在 256 个变量、4096 个对象、64K 的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好嘛?但是如果环境中就是有这么多变量一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法玩了。

微软在 IE7 中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的 15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临界条件翻倍,如果回收的内存高于 85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作智能了很多。

2、合理的 GC 方案
1)、JavaScript 引擎基础 GC 方案是(simple GC):mark and sweep(标记清除),即:

  • 遍历所有可访问的对象。
  • 回收已不可访问的对象。

2)、GC 的缺陷
和其他语言一样,JavaScript 的 GC 策略也无法避免一个问题:GC 时,停止响应其他操作,这是为了安全考虑。而 JavaScript 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于 JS 游戏,动画连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免 GC 造成的长时间停止响应。

总结

一般不用 setInterval,而用 setTimeout 的延时递归来代替 interval。
setInterval 会产生回调堆积,特别是时间很短的时候。

扩展

setInterval 有个很烦的地方就是当 js 主程序空闲的时候,执行代码队列里面的代码的时候,如果此时候我们有一个问题,定时器是等到回调执行完,才开始计时进行下次循环呢?还是只要一次计时完毕,插入回调之后不管回调执不执行就开始计时呢?答案显示是后者,这也就是我说 setInterval 坑的原因啊,因为这会出现一种情况,当我们插入回调的时候前队列有别的代码在执行,这时候回调肯定是不会执行的,因此如果这个时候无限定时时间到了会再次插入回调,这个时候如果发现队列中的第一次回调没有执行,那么再次插入的回调浏览器就默认取消,(这是以防出现回调连续执行多次的情况)但是这又引发了新的情况就是有些回调是不能取消掉的?

-------------本文结束 感谢您的阅读-------------
作者Neveryu
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分