垃圾的定义

​ 在js内存管理策略中它会对失去可达性(无引用或者无法通过某种方式进行访问)的空间进行回收。即失去可达性的内存空间会被视为是垃圾。

​ 在正常开发中,我们不可避免的会在程序中进行各种变量声明函数创建等操作,这些操作无疑都是需要从浏览器处进行内存申请,浏览器进行内存分配,但是如果我们在使用引用类型变量时更改了其引用地址,就导致之前被引用的堆地址失去可达性而被白白占用(参考下面代码例子),如果变量变多这种被白白占用的地址会越来越多,导致新的变量能分配使用的内存会越来越小,系统运行速度会越来越慢,达到临界值时会导致系统崩溃。

1
2
3
4
5
6
7
let ceshi = {
a: 1,
b: 2

}
// 上方引用地址无变量引用导致白白占用内存空间
ceshi = [1, 2, 3, 4, 6]

JS中垃圾回收策略

1. 标记清除

​ 在代码执行阶段,为程序中所有的变量添加上一个二进制字符(二进制运算最快)并初始值置为0(默认全是垃圾),然后遍历所有的对象,被使用的变量标记置为1,在程序运行结束时回收掉所有标记为零的变量,回收结束之后将现存变量标记统一置为0,等待下一轮回收开启。

优点:标记清除算法思路清晰,实现比较简单。

缺点:由于系统分配的内存时间不同,回收的先后顺序也是不同的,这时就会导致剩余空闲空间并不是连续的,出现了内存碎片现象。内存碎片过多容易导致后续出现分配失败的情况。

2. 标记整理(为解决标记清除存在的问题)

​ 标记整理的清除逻辑和标记清除算法基本相似不过进行了优化,会在清除结束之后将活着的空间进行整理向一端移动,同时清理掉内存的边界。

3. 引用计数

​ 引用计数跟踪记录每个变量值被使用的次数,如果一个引用类型的值给一个声明的变量赋值,则将这个引用类型的值的引用次数加1,如果同一个值又被赋值给另一个变量,则引用计数再加1,如果之前被赋值的变量值变更成了其他引用类型,则原本的引用类型引用计数减1,如果这个引用类型的引用计数为0时表示,此时为不可达状态,浏览器垃圾回收器就将此类型占用的空间进行回收(此处是实时的,当计数变为0既会被立即回收)。

优点:1、实时回收,引用计数当归零就立即进行回收操作。 2、不会暂停执行栈,标记清除算法定时进行垃圾回收时会先暂停程序运行,来进行垃圾回收,而引用计数是实时回收不会暂停程序的运行

缺点:1.需要开辟空间来存储计数器,同时由于引用无上限故占用空间也是无上限。 2、无法解决循环引用无法回收(致命问题),循环引用指的是:两个引用类型AB,A有一个地址指向了B,B也有一个对象指向了A,导致两者引用计数为2,正常情况下当test函数运行结束进行垃圾回收,但是AB两者的基数都是不是0则回收失败,无法清除,这种情况大量发生时会造成大量的内存空间被浪费,故引用计数算法现在已经很少使用逐渐被标记清除算法替代。

1
2
3
4
5
6
7
// 循环引用示例
function test() {
let A = new Object()
let B = new Object()
B.a = A
A.b = B
}

V8中对垃圾回收的优化

1. 分代式优化

​ 对于存活时间不同的对象应该使用不同的机制进行内存回收。对于新创建的,体积小和存活时间短的对象需要更加频繁的检查;而对于体积大,存活时间长,创建早的内存则无需频繁检查。

​ 基于此,V8提出了新生代和老生代优化策略,将内存空间划分为新生代和老生代两个部分,不同部分执行不同的回收策略。

新生代与老生代

新生代

​ 新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量。 而新生代中的内存又会被拆分为两部分,使用区和空闲区,浏览器进行内存申请时分配使用区空间,当使用区空间快被写满时则进行一次垃圾回收,新生代的垃圾回收器会对使用区的活动对象进行标记,标记完成之后将使用区活跃的对象复制到空闲区,并进行排序,随后进入垃圾清理阶段,对使用区进行清理,清理操作完成之后,使用区和空闲区进行角色互换,之前的空闲区变成新的使用区,之前的使用区变成新的空闲区,循环往复。

​ 当一个对象被多次复制还未被清理掉,故此对象会被认定为生命周期较长的对象,会被从新生代移动到老生代中,采用老生代的垃圾回收机制管理。

老生代

​ 相比于新生代,老生代顾名思义存放的就是一些生命周期比较长,经过多次新生代垃圾回收还存在的对象,同样的相比于新生代不仅垃圾回收频率较低,存储空间也是比新生代大的多。而老生代的回收算法就比较简单就是标记清除算法,不过在v8中为了处理标记清除算法产生的内存碎片问题,使用了标记整理算法进行空间优化大大提高了回收效率。

2. 并行回收

​ 众所周知JavaScript是一门单线程语言,所以在进行GC回收时会阻塞js脚本的运行导致系统停顿,等GC回收结束后恢复运行,这被称为全停顿。

​ 但是这样的话会存在极大的风险,如果GC回收时间较长,就会导致系统停顿时间较长这是不可被接受的。所以V8引擎加入了并行回收的优化机制,在开启GC回收线程之后,会同时开启多个辅助线程进行处理,提高回处理时间,虽然增加了一部分线程之间协调的时间,但是总时间比一个线程用时来讲大大的缩短。避免系统卡顿时间过长。

3. 增量标记(让垃圾回收过程变成可中断)

​ 由于全停顿标记策略在处理老生代垃圾回收时即使是有并行处理优化但是消耗时间也会消耗大量的时间,所以在2011年时V8团队又提出了增量标记策略来进行优化。增量标记思想就是将一次GC标记过程进行拆分,一次执行一小部分,执行完毕后继续执行脚本,执行一段脚本之后又继续执行刚刚拆分的GC标记任务,循环往复直至这次GC标记完成。

4. 三色标记法(恢复与暂停)

​ 在引入三色标记法之前的GC标记只是将活动的变量标记为黑色,不活动的变量标记为白色,当GC标记过程结束之后,系统会回收掉所有的白色标记变量,但是这种非黑即白的方法虽然清除起来非常方便但是存在一个问题执行一段时间之后无法知道执行到了哪里,不能进行暂停。所以V8又引入了一个灰色进行暂停和恢复操作。

三色标记法

​ 如图所示,在GC标记开始时所有对象都是白色的,然后从根对象开始进行标记,先将这组对象标记为灰色然后进行记录,如果此时进行中断,后续恢复时既从灰色标记时开始即可,当回收器从标记工作表中弹出对象并访问他们的引用对象时,会将灰色置为黑色,同时将下一个引用对象置为灰色,继续往下进行标记工作。直至无可标记为灰色对象为止,此时表示GC标记过程结束,将所有未标记的变量进行回收工作。所以三色标记法可以渐进执行而不用每次执行都要全盘进行扫描整个内存空间,可以配合增量回收减少全停顿时间,提升体验。

5. 写屏障

​ 在一次完成GC标记暂停中,如果执行任务程序时内存中存在的变量引用关系被改变了,这样会导致此次GC存在问题。如图所示,现有A、B、C三个对象依次被引用,且在GC过程中已经被标记了,但是在暂停GC任务,插入执行程序任务之后,引用关系被改变了,新增了一个新变量D,但是此时程序中也未存在灰色标记的变量,下一步进行清除机制时,新变量D按清除机制来讲是要被清除掉,但是这是极其不合理的,一个新的变量还存在引用就被回收掉,这会导致程序云行报错。此时写屏障机制就派上用场了,一旦有黑色的对象引用白色的对象,就会强制将被引用的白色变量标记为灰色,保证下一次的增量GC正确运行,这个机制称为强三色不变性(白色变量D被黑色变量B引用之后会被强制置灰保证程序运行正确性)。

写屏障

6. 惰性清理

​ 在增量GC标记之后下一步就是来真正回收内存空间,通过惰性清理来进行清除释放内存。惰性清理机制运行原理是在进行回收时如果内存足够就可以将这个回收清理时间稍微延迟一下,让JavaScript脚本先执行,清理时也不会一下全部清理掉所有的垃圾,会按需进行清理直至所有垃圾都回收完毕,然后继续等待下个GC标记阶段执行结束。