垃圾回收机制

本文最后更新于:8 个月前

V8的垃圾回收机制,内存泄漏是什么?如何避免内存泄漏?

垃圾回收&内存泄漏

虽然垃圾回收是引擎自动完成的工作,但是我们依然需要了解它,意义就在于:了解之后,我们能够清楚如何主动规避一些不利于引擎做GC的操作,从而避免内存泄露

一、什么是GC

Garbage Collection,垃圾收集。程序运行过程会产生很多不用的内存、或者用过了不再使用的内存,我们称之为“垃圾”。GC垃圾回收,就是指回收这些没有用的内存空间。这个过程是由浏览器自动完成的。

高级语言里会自带GC,如:Java、Python、JavaScript。但是C、C++没有。

JS里面区分栈内存、堆内存,分别保存不同的数据类型,那么垃圾回收机制在这两者上是否有区别呢?

二、垃圾是怎么产生的

举个例子:

1
2
let test = {name:'old'}
test = [1,2,3]

第一行代码在内存中创建了一个对象A,

第二行代码,当test执行另一个数组B时,对象A就失去了作用,它就成为了内存“垃圾”

三、V8的GC

1、标记清除算法

如何确定内存中那些内容是应该删除的垃圾?

概念:可达性

以某种方式可以访问到的值,就是具有可达性的值,它们应该保存在内存中。反之,不可达的值则需要回收,释放对应的内存空间。至于如何发现不可达的值,V8引擎采用的是标记清除算法。定期执行该算法来GC

  • 标记阶段

    第一步,给内存中所有值标记一个0值

    第二步,从一组「根」对象(如Window对象、DOM树)出发,遍历内存中所有对象,标记上1值

  • 清除阶段

    第三步,销毁所有标记为0的值,回收对应的内存空间

优点:实现简单

缺点:回收后的内存空间不连续(下面就解决这个问题)

2、标记整理算法

据说标记阶段与「标记清除算法」的过程一样,但是不太懂为什么整理内存还要进行标记

该算法达到的效果是:将仍存在在内存中的对象向内存一侧移动,从而清出大片完整的空闲内存。

3、V8的GC优化

  • 优化背景:

    如果对内存中体积大、存活时间长的对象,与体积小、存活时间短、较新的内存对象采用同样的检查频率显然不够合理。

  • 优化方式:

    分代垃圾回收,区分新老生代,将堆内存分为两部分,分别保存新老两代的对象

    新生代:清理频率高,内存较小,保存存活时间短、新产生的对象

    老生代:清理频率低,内存较大,保存存活时间长、体积较大的对象

    对于新老两块内存区域的垃圾回收,V8 采用了两个垃圾回收器来管控

    image-20230509181902106

4、新生代GC

  • 回收算法:Scavenge算法 + 并行回收(多线程)

  • 回收过程:将(新生代)堆内存一分为二:「使用区」、「空闲区」,将新对象都放入使用区、快写满的时候执行垃圾清理

    第一步,标记使用区中的活动对象

    第二步,将使用区的活动对象复制进空闲区并排序(为什么要排序?)

    第三步,将使用区内所有对象销毁,并转换「使用区」和「空闲区」(即原来的空闲区变成现在的使用区)

5、老生代GC

  • 回收算法:标记清除算法、标记整理算法 + 增量标记(三色标记法+写屏障)

  • 回收过程

    第一步,将(老生代)堆内存中所有对象标记为0

    第二步,从一组根元素开始,遍历内存中的活动对象,标记为1

    第三步,清除所有标记为0的对象

    第四步,执行标记整理算法,清理内存碎片

  • 「增量标记」带来的问题:

    如果仍然使用0、1两种标记,将无法获悉下一个标记阶段的起点 ==》 「三色标记法」

  • 「三色标记法」

    黑色:标记过的活动对象

    白色:标记过的非活动对象 、 还没有标记的对象

    灰色:正在进行标记的对象(下个标记阶段的起点)

  • 「三色标记法」存在的问题:增量中引用发生修改

    为黑色对象添加一个新的引用对象,该对象默认为白色,将会被清除,造成访问不到的错误 ==》 写屏障

    image-20230510115312255

  • 「写屏障」

    一旦有黑色引用白色对象,白色对象强制变为灰色,确保下一次增量GC时能被正确标记

四、什么是「内存泄漏」

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

这里需要对「闭包」特别说明一下。虽然闭包也会占用着内存不释放,但这是属于我们预料之内的,所以应该不能将「闭包」归入「内存泄漏」的范畴。当然这也不是说可以随便使用、滥用闭包了。

五、如何发现「内存泄漏」

可以参考这篇文章:手把手教你排查Javascript内存泄漏 - 知乎 (zhihu.com)

六、导致「内存泄漏」的错误示例

1. 被遗忘的计时器

setTimeoutsetInterval两个定时器如果没有clearTimeout/clearInterval注销掉,它内部的回调函数和依赖的变量都不能被GC回收,所以会导致「内存泄漏」。

解决方式

  • 不再需要定时器事件事,手动将其注销

2.不必要的全局变量

全局变量绑定在window对象上,在网页整个生命周期内都属于可达到活动对象,所以不会被GC销毁。

除了尽量避免使用全局变量之外,还需要注意不要出现意外的全局变量,如下:

1
2
3
4
// 没有使用声明关键字,导致bar变量被绑定到window全局对象上
function fn(){
bar = {name:'error'}
}
1
2
3
4
5
6
// 注意:这里要与构造函数实例区分开来,构造函数实例时,是需要使用new关键字的(而且函数一般以大写字母开头)
// 这里只是调用函数,实际上下例中的this指向window全局对象
function fn(name){
this.name = name
}
fn()

解决方式:

  • (1)使用严格模式,在js文件头部添加use strict,可以避免“意外的全局变量”
  • (2)将不在使用的全局变量赋值为null,可以释放掉对应的内存

3.频繁的console输出

console使用的对象是不能被垃圾回收的,导致内存泄漏

4.DOM泄漏

1
2
3
4
var a = document.getElementById('id');
document.body.removeChild(a);
// 虽然id这个DOM节点被删除了,但是因为变量a对这个DOM节点的引用还在,所以导致DOM节点无法被GC回收
// 解决办法: a=null

参考链接

「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)

深入浅出 js内存泄漏 - 掘金 (juejin.cn)


垃圾回收机制
http://timegogo.top/2023/05/09/JavaScript/JavaScript(六)垃圾回收机制/
作者
丘智聪
发布于
2023年5月9日
更新于
2023年7月16日
许可协议