JavaScript深入理解之垃圾收集

JavaScript深入理解之垃圾收集 文章配图

写在前面

JavaScript 具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存。那么这种机制的具体实现原理是怎样的呢?下面是我的一些理解和总结。

原理

JavaScript 垃圾收集机制的原理其实很简单,就是找出那些不再继续使用的变量,然后释放其占用的内存。为此垃圾收集会按照规定的时间间隔(或代码执行中预付的收集时间),周期性地执行这一操作。

但是该如何判断一个变量是否还有存在的必要呢?垃圾收集器必须跟踪哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。

标记清除

定义

JavaScript 中最常用的垃圾收集方式是标记清除。它的具体工作步骤如下:

  1. 给存储在内存中的所有变量加上标记(当然可以使用任何标记方式)

  2. 去掉当前执行环境中的变量,以及被执行环境中的变量引用的变量的标记

  3. 第二步结束后仍被标记的变量将被视为准备删除的变量,因为此时的执行环境中的变量已经无法访问到这些变量了。

  4. 完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

举个例子

下面我们来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = 1,
b = 2;

function test1(){
var c = 3;
}


function test2(){
var d = 4;

console.log(a);
console.log(b);
}


test1();

test2();

假设当我们执行到test2()函数中console.log(b);语句时,垃圾收集器开始执行了,那么此时的内存占用情况是怎样的呢?请看下图

配图21-2

上面仅列举了部分内存占用情况,只用作讲解标记清除的原理。

  1. 那我们首先开始第一步,将内存中的所有变量加上标记,这里以红色作为标记。

    peitu21-2

  2. 然后我们去掉当前执行环境中的变量,以及被执行环境中的变量引用的变量的标记。简单分析一下,此时执行环境中可以访问的变量有 d ,然后我们通过作用域链可以访问到全局变量对象,因此 a 和 b 我们也是可以访问到的,因此我们需要将这三个变量的标记去掉。

    配图21-3

  3. 此时只有 c 变量还保留有标记,说明 c 变量通过此时的环境已经访问不到了,所以 c 变量需要被清除掉来释放内存。

  4. 最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

    配图21-4

缺点

  1. 标记清除法的第一个问题就是效率不高,因为在标记清除-阶段,整个程序将会等待,所有如果程序出现卡顿的情况你,那么就有可能是收集垃圾的情况。

  2. 标记清除法的第二个问题是,从上面的例子我们可以看出,在清除之后内存空间不是连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。与标记清除法相比,标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。

我的理解

因为标记清除法并不是 JavaScript 垃圾收集方法,很多其他语言也采用了这样的算法。这种算法的机制是这样的

这个算法假定设置一个叫做根(root)的对象。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。

我在网上看见有的人把根对象理解为了 JavaScript 中的全局对象。我认为这样去理解是解释不通的。因为在运行的某一时刻,垃圾收集收集器开始运行,通过全局对象是没有办法访问到整个引用链的。全局对象和其他对象间并不是一个树形的关联关系。

根据 JavaScript 高级程序设计中对标记清除的描述,我认为将这个跟对象理解为当前的执行上下文对象比较准确,因为通过当前的执行上下文对象可以访问到当前环境中的变量,也可以通过作用域链去访问到其他上下文中的活动对象,从而可以判断得到所有可以获得的对象和所有不能获得的对象。

当然这仅属于我个人的看法,欢迎大家交流指教。

引用计数

定义

另一种不太常见的垃圾收集策略叫做引用计数,引用计数的含义就是跟踪记录每个值被引用的次数。

引用计数的原理是,当一个引用类型值被一个变量引用时,那么这个值的引用次数加一,如果该变量不再引用该值,则这个值的引用次数减一。当一个值的引用次数变为0的时候,就说明,没有变量引用该值,该值的内存空间可以被回收了。这样一来,当垃圾清除器下次运行时,它就会释放那些引用次数为0的值的内存。

如下面的例子所示

1
2
3
var obj = { a: 1}; // 一个对象被创建,且 obj 引用了这个对象,因此该对象此时的引用次数为1,不能被清除

obj = null; // 此时,obj 不再引用该对象,则该对象的引用次数减为0,可以被清除

缺点

引用计数的最大问题就是循环引用的问题。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 中的引用。如下面的例子所示:

1
2
3
4
5
6
7
function example(){
var obj1 = new Object(); // 一个对象 A 被创建,并被 obj1 引用,此时对象 A 的引用次数为1
var obj2 = new Object(); // 一个对象 B 被创建,并被 obj2 引用,此时对象 B 的引用次数为1

obj1.otherObject = obj2; // 对象 B 又被 obj1.otherObject 引用,此时对象 B 的引用次数为2
obj2.otherObject = obj1; // 对象 A 又被 obj2.otherObject 引用,此时对象 A 的引用次数为2
}

因为循环引用的问题,当函数执行完毕后对象 A 和对象 B 还将继续存在,因为它们的引用次数永远不会为0,这就意味着它们永远不会被回收,导致内存泄漏。

要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

1
2
obj1 = null;
obj2 = null;

写在最后

这篇文章只是对 JavaScript 垃圾收集的一个简单的总结。其中涉及到的很多都只是原理的理解,对于其细节的深入理解我认为没有太多必要。理解 JavaScript 垃圾收集机制对我们理解 JavaScript 中一些其他概念时也有很大的帮助。

本篇文章纯属于个人的学习总结,如果文章中出现错误或不严谨的地方,希望大家能够指出,谢谢!

坚持原创技术分享,您的支持将鼓励我继续创作!