JavaScript垃圾回收

javascript垃圾回收主要分成两种方法:

1. 引用计数

引用

  • 垃圾回收算法主要依赖于引用的概念。

  • 在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。

  • 例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

  • 在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域。

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了

2. 标记清除

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

  1. 给存储在内存中的所有变量加上标记(当然可以使用任何标记方式)
  2. 去掉当前执行环境中的变量,以及被执行环境中的变量引用的变量的标记
  3. 第二步结束后仍被标记的变量将被视为准备删除的变量,因为此时的执行环境中的变量已经无法访问到这些变量了。
  4. 完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

对于如下代码:

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

function test1(){
var c = 3;
}

function test2(){
var d = 4;
console.log(a);
console.log(b);
}

test1();

test2();

假设执行到test2()时,垃圾回收开始执行

对于此时,内存情况为

image-20210412150846546

开始第一步,将内存中的所有变量加上标记

image-20210412151850990

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

image-20210412152201055

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

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

image-20210412152245827

缺点:

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

对比一下官方文档的解释:

标记清除这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象

这里的‘根’指的应该是上下文对象

因为通过当前的执行上下文对象可以访问到当前环境中的变量,也可以通过作用域链去访问到其他上下文中的活动对象,从而可以判断得到所有可以获得的对象和所有不能获得的对象