跳到主要内容

垃圾回收机制

垃圾回收策略

引用计数算法

引用计数,这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它

  • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
  • 如果同一个值又被赋给另一个变量,那么引用数加 1
  • 如果该变量的值被其他的值覆盖了,则引用次数减 1
  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

缺点

  1. 无法解决循环引用无法回收的问题

标记清除算法

标记清除,目前在 JavaScript 引擎里 这种算法是最常用的

此算法分为 标记清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为 0
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成 1
  • 清理所有标记为 0 的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为 0,等待下一轮垃圾回收

缺点

  1. 在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片

V8 对 GC 的优化

新老生代

V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1 ~ 8M 的容量,

而老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大

新生代垃圾回收

该算法将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区

新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作

当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区

当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理

另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配

老生代垃圾回收

相比于新生代,老生代的垃圾回收就比较容易理解了,上面我们说过,对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,如果再如新生代一般分区然后复制来复制去就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是上文所说的标记清除算法了

首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象

清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉

参考