写在前面
JavaScript 具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存。那么这种机制的具体实现原理是怎样的呢?下面是我的一些理解和总结。
原理
JavaScript 垃圾收集机制的原理其实很简单,就是找出那些不再继续使用的变量,然后释放其占用的内存。为此垃圾收集会按照规定的时间间隔(或代码执行中预付的收集时间),周期性地执行这一操作。
但是该如何判断一个变量是否还有存在的必要呢?垃圾收集器必须跟踪哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。
标记清除
定义
JavaScript 中最常用的垃圾收集方式是标记清除。它的具体工作步骤如下:
给存储在内存中的所有变量加上标记(当然可以使用任何标记方式)
去掉当前执行环境中的变量,以及被执行环境中的变量引用的变量的标记
第二步结束后仍被标记的变量将被视为准备删除的变量,因为此时的执行环境中的变量已经无法访问到这些变量了。
完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
举个例子
下面我们来看一个例子
1 | var a = 1, |
假设当我们执行到test2()函数中console.log(b);
语句时,垃圾收集器开始执行了,那么此时的内存占用情况是怎样的呢?请看下图
上面仅列举了部分内存占用情况,只用作讲解标记清除的原理。
那我们首先开始第一步,将内存中的所有变量加上标记,这里以红色作为标记。
然后我们去掉当前执行环境中的变量,以及被执行环境中的变量引用的变量的标记。简单分析一下,此时执行环境中可以访问的变量有 d ,然后我们通过作用域链可以访问到全局变量对象,因此 a 和 b 我们也是可以访问到的,因此我们需要将这三个变量的标记去掉。
此时只有 c 变量还保留有标记,说明 c 变量通过此时的环境已经访问不到了,所以 c 变量需要被清除掉来释放内存。
最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
缺点
标记清除法的第一个问题就是效率不高,因为在标记清除-阶段,整个程序将会等待,所有如果程序出现卡顿的情况你,那么就有可能是收集垃圾的情况。
标记清除法的第二个问题是,从上面的例子我们可以看出,在清除之后内存空间不是连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。与标记清除法相比,标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。
我的理解
因为标记清除法并不是 JavaScript 垃圾收集方法,很多其他语言也采用了这样的算法。这种算法的机制是这样的
这个算法假定设置一个叫做根(root)的对象。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。
我在网上看见有的人把根对象理解为了 JavaScript 中的全局对象。我认为这样去理解是解释不通的。因为在运行的某一时刻,垃圾收集收集器开始运行,通过全局对象是没有办法访问到整个引用链的。全局对象和其他对象间并不是一个树形的关联关系。
根据 JavaScript 高级程序设计中对标记清除的描述,我认为将这个跟对象理解为当前的执行上下文对象比较准确,因为通过当前的执行上下文对象可以访问到当前环境中的变量,也可以通过作用域链去访问到其他上下文中的活动对象,从而可以判断得到所有可以获得的对象和所有不能获得的对象。
当然这仅属于我个人的看法,欢迎大家交流指教。
引用计数
定义
另一种不太常见的垃圾收集策略叫做引用计数,引用计数的含义就是跟踪记录每个值被引用的次数。
引用计数的原理是,当一个引用类型值被一个变量引用时,那么这个值的引用次数加一,如果该变量不再引用该值,则这个值的引用次数减一。当一个值的引用次数变为0的时候,就说明,没有变量引用该值,该值的内存空间可以被回收了。这样一来,当垃圾清除器下次运行时,它就会释放那些引用次数为0的值的内存。
如下面的例子所示
1 | var obj = { a: 1}; // 一个对象被创建,且 obj 引用了这个对象,因此该对象此时的引用次数为1,不能被清除 |
缺点
引用计数的最大问题就是循环引用的问题。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 中的引用。如下面的例子所示:
1 | function example(){ |
因为循环引用的问题,当函数执行完毕后对象 A 和对象 B 还将继续存在,因为它们的引用次数永远不会为0,这就意味着它们永远不会被回收,导致内存泄漏。
要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
1 | obj1 = null; |
写在最后
这篇文章只是对 JavaScript 垃圾收集的一个简单的总结。其中涉及到的很多都只是原理的理解,对于其细节的深入理解我认为没有太多必要。理解 JavaScript 垃圾收集机制对我们理解 JavaScript 中一些其他概念时也有很大的帮助。
本篇文章纯属于个人的学习总结,如果文章中出现错误或不严谨的地方,希望大家能够指出,谢谢!