1. 标记清除
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改为1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
1.1 优点
实现简单,使用一位二进制位即可标记
1.2 缺点
- 内存碎片化:在清除之后,剩余对象内存位置不变,会导致空闲内存不连续,出现内存碎片,并且由于剩余空闲内存不是一整块,它是由不同内存组成的内存列表,会牵扯出内存分配的问题
- 分配速度慢:即便是First-fit策略,其操作依旧是O(n)的操作
- 分配策略
- First-fit,找到大于等于size的块立即返回
- Best-fit,遍历整个空闲列表,返回大于等于size的最小分块
- Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
1.3 标记整理算法
它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将不需要清理的对象向内存的一端移动,最后清理掉边界的内存
2. 引用计数
-
当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
-
如果同一个值又被赋给另一个变量,那么引用次数加 1
-
如果该变量的值被其他的值覆盖了,则引用次数减 1
-
当这个值的引用次数变为 0 的时候,垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存
2.1 缺点
无法解决循环引用的问题
function test(){
let A = new Object()
let B = new Object()
A.b = B
B.a = A
}
/**
* 按照引用计数策略,它们的引用数量都是 2,但是,在函数 test 执行完成之后,
* 对象 A 和 B 是要被清理的,但使用引用计数则不会被清理,因为它们的引用数量不会变成 0,
*假如此函数在程序中被多次调用,那么就会造成大量的内存不会被释放
**/
2.2 优点
引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
3. V8垃圾回收机制
3.1 分代式垃圾回收
分代式垃圾回收是为了优化对于一些大、老、存活时间长的对象来说同新、小、存活时间短的对象一个频率的检查的问题,因为前者需要时间长并且不需要频繁进行清理,后者恰好相反。
V8中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器。新生代的对象为存活时间较短的对象,即新产生的对象,通常只支持1~8M的容量,而老生代的对象为存活时间较长或常驻内存的对象,即经历过新生代垃圾回收后还存活下来的对象,容量通常比较大
3.1.2 新生代垃圾回收
新生代对象是通过一个名为 Scavenge
的算法进行垃圾回收,在 Scavenge算法
的具体实现中,主要采用了一种复制式的方法即 Cheney算法
。Cheney算法
中将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区
,一个是处于闲置状态的空间我们称之为 空闲区。
新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
在垃圾回收过程中,首先要对使用区域中的垃圾做标记。标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些 活动对象 复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
完成复制后,使用区域与空闲区域进行角色翻转,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge
回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
3.1.2 老生代垃圾回收
标记清除法
3.2 性能优化
全停顿:由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做 全停顿(Stop-The-World)。
3.2.1 并行回收
新生代的垃圾回收采取并行策略提升垃圾回收速度,它会开启多个辅助线程来执行新生代的垃圾回收工作。并行执行需要的时间等于所有的辅助线程时间的总和加上管理的时间。并行执行的时候也是全停顿
的状态,主线程不能进行任何操作,只能等待辅助线程的完成
这个主要应用于新生代的垃圾回收
3.2.2 并发回收
并发就是在 JS 主线程运行的时候,同时开启辅助线程,清理和主线程没有任何逻辑关系的垃圾。
3.2.3 增量标记
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为 增量标记(Incremental Marking)算法。
为了支持增量标记,V8必须支持垃圾回收的暂停
和恢复
,这里采用了黑白灰三色标记法
- 黑色表示这个节点被垃圾回收根引用到了,而且该节点的子节点都已经标记完成了
- 灰色表示这个节点被 垃圾回收根引用到了,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点
- 白色表示此节点还没未被垃圾回收器发现,如果在本轮遍历结束时还是白色,那么这块数据就会被收回
引入黑白灰标记法之后,就可以通过标记判断,重新开始或是暂停正在进行的标记。
3.2.4 惰性清理
增量标记完成之后,惰性清理
才是真正能够清理垃圾的阶段。当我们的内存能够使我们流畅的运行代码,其实我们是没有必要进行清理内存的,它会稍稍延迟一下清理过程,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象都清理完毕。