在其他的一些语言里(C、C++),内存的申请和释放都需要开发人员自己去控制,这无疑会大大增加开发的工作量,但是在 javascript 的世界里,它会自动地帮我们完成垃圾回收(GC),所以GC对于我们前端工程师来说是透明的,但是我认为深入了解js的GC还是很有必要的,因为只有当我们真正地掌握了其流程和原理,才能在平时的开发中更加自信地面对各种疑难杂症,从而体现我们自己的价值😎

废话不多说,开搞!

bqb

在javascript的世界里,GC的实现方式主要分为两种:

  1. 引用计数
  2. 标记清除

现在绝大部分的现代浏览器都是基于标记清除或者其变体来实现的GC,但是在IE9及之前的版本中是采用引用计数的方式来实现GC

那么为什么现代浏览器会抛弃引用计数而都使用标记清除呢?

要想知道这个答案,我们得先了解两种方式的实现以及各自的优缺点,本篇文章就先来聊聊 引用计数 这种方式

bqb

引用计数

引用计数的实现方式很简单,就是对于每一个引用类型的值,每多一次变量引用就+1,少一次变量引用就-1,具体示例如下:

1
2
3
4
5
var a = {}  //有一个变量指向该对象,所以计数+1,此时引用次数为1
var b
b = a //又多了一个变量指向创建的对象,所以计数+1,此时引用次数为2
a = null //变量a不再指向创建的对象,所以计数-1,此时引用次数为1
b = null //变量n不再指向创建的对象,所以计数-1,此时引用次数为0

这种方案存在两个缺点:

  1. 由于无法预估某一个引用类型被引用的次数,所以需要预留较大的空间来存放计数的结果
  2. 如果代码中存在循环引用的场景,那么就无法正确回收内存,从而导致内存泄漏,示例如下:
1
2
3
4
5
6
7
function test(){
let A = new Object()
let B = new Object()

A.b = B //此时B引用次数为1
B.a = A //此时A引用次数为1
}

当示例代码中的 test 函数执行完成后,本应该释放的A,B对象,由于互相引用的原因,导致引用次数不为0,最终导致GC无法正确回收A,B对象所占用的内存空间,从而导致内存泄漏,也正是由于这个缺点,导致现代浏览器放弃了这种方案

但这种方案也不是一无是处,它也有如下三个优点:

  1. 垃圾是及时回收的,也就是说当某一个引用类型的值的引用次数为0时,GC会立刻回收这个引用类型的值所占用的空间
  2. 正是由于垃圾回收是及时的,所以不会单独耗费时间去进行GC
  3. 也正是由于这种机制,所以也不需要为了去区分活动对象和非活动对象而再去专门遍历堆里的所有对象

结语

虽然引用计数这种方案已经过时了,但是了解其原理以及优缺点后,能让我们更清楚地知道为什么会设计出 标记清除 的方案以及它是如何去解决 引用计数 所带来的的问题的,这样我们才能知其然,也能知其所以然

欲知 标记清除 的门道,请听下回分解😉