【面试补缺(1)】JS垃圾回收机制

面试是最好的照妖镜,能够从专业人士的眼光中看出自己哪些还有不足。这个系列就用来记录一下容易被忽视掉的基础。

参考自:Understanding JavaScript Memory Management using Garbage Collection

目标

垃圾回收机制不是一个新名词,主要就是由引擎来对我们分配出去的内存空间做统一的管理和回收。希望通过这篇文章的总结,可以了解以下几个问题:

  1. JS的垃圾回收算法是什么
  2. 垃圾回收的时机
  3. 能不能人为干预JS的回收机制

JS的垃圾回收机制

JS的垃圾回收机制主要是从内存中删除那些无法到达的对象。主要由下面两种算法实现:

  • 引用计数回收
  • 标记扫描算法

引用计数回收

实例

这是一个很简单的算法。主要就是看哪些对象没有被引用。如果一个对象没有被任何引用指向的话,那么它就要被回收。

1
2
3
4
5
var obj1 = {
property1: {
subproperty1: 20
}
}

创建如上图的代码例子。在这个例子中,obj1有一个property1对象,而property1也有一个subproperty1对象。因为obj1有引用指向到对象,因此不会被回收掉。

1
2
var obj2 = obj1
obj1 = "some random text"

现在,obj2同样指向了obj1所指向的对象,但是随后obj1就被更新成了"some random text"的一个字符串,因此obj2就成了唯一指向那个对象的引用。

1
var obj_property1 = obj2.property1

现在obj_property1指向了obj2.property1,同样持有该对象的引用。就是说,当前的对象拥有两个引用:

  1. obj2指向对象本身
  2. obj_property1指向对象的子对象property1
1
obj2 = "some random text"

obj2解除引用并更新为"some random text"字符串。那么,它之前所指向的对象看起来就已经没有了引用,应该能被回收了吧?实际上并不是的,因为obj_preperty1依然指向着这个对象的子对象obj2.preperty1,因此它是不会被回收的。

1
obj_property1 = null

obj_preperty1设为null,那么最原始的对象就真的没有了任何引用了。所以就可以被回收掉了。

什么时候算法会失效?

试试看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function example() {
var obj1 = {
property1: {
subpreperty1: 20
}
}

var obj2 = obj1.property1
obj2.property1 = obj1
return 'some random text'
}

example()

这个例子就可以看到,当函数执行完之后,obj1obj2依然没有释放内存,因为它们互相调用,形成了回路。

回收的时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<body>
<button onclick="gc()">开始回收</button>
<script>
var as = []
for (let i = 0; i < 1000000 ; i++) {
as[i] = {b:i} //document.createElement('div')
}

function gc () {
console.log('start gc')
for (let i = 0; i < 1000000 ; i++) {
as[i] = null
}
as = null
}

</script>
</body>
</html>

执行以上测试代码发现,JS heap size会飙升到87MB,然后执行gc,但是并不是马上会回收掉内存,根据观察,每次回收的时间都不相同,这就有点奇怪了,有时候会几秒后就马上被回收,有时候等上一分钟才回收,可能和引擎自身的循环机制有关,这部分找不到相关的资料。

标记扫描算法

实例

这个算法会从根结点开始(也就是Javascript的全局对象)去遍历查找那些不能被访问到的对象。这个算法克服了引用计数回收算法的缺点。一个对象没有被引用将不能够被访问,不能被访问的对象也就是没有被引用。

1
2
3
var obj1 = {
property1: 35
}

root-to-object

如上图所示,我们的对象是能够被root搜索访问到的

1
obj1 = null

root-can-not-to-object

现在可以看到,把obj1设为null之后,ROOT再也不能访问到这个对象,因此它会被回收掉。

这个算法从ROOT节点开始向下遍历,并标记所有它能够遍历到的对象节点,并且继续遍历这些节点的子节点。此过程一直持续到没有其他子节点或者路径可以遍历的时候才停止。
现在,垃圾回收器会忽略那些被标记的节点,把其它没有被标记的节点清掉。
clean-unmarked

可以看到,右面的节点子树是root节点开始遍历所遍历不到的,所以会被回收掉。