引用计数
引用计数(Reference Counting)算法是每个对象计算指向它的指针的数量,当有一个指针指向自己时计数值加1;当删除一个指向自己的指针时,计数值减1,如果计数值减为0,说明已经不存在指向该对象的指针了,所以它可以被安全的销毁了。可以很直观的用下面的图表示:
但是如果存在循环引用的情况,循环引用链上的对象的计数都无法变为0,所以这种情况是无法回收的
优点:
- 垃圾对象内存回收及时性高:一旦发现对象的计数为0,立即回收内存
- 最大限度避免程序因为内存占用过大导致的挂起
缺点:
- 无法释放循环引用的对象:对象互相持有,导致计数无法归零
- 时间开销大:因为要时刻维护对象的引用计数,所以时间成本上要高一些
标记清除
标记清除(Mark-Sweep)算法依赖于对所有存活对象进行一次全局遍历来确定哪些对象可以回收,遍历的过程从根出发,找到所有可达对象,除此之外,其它不可达的对象就是垃圾对象,可被回收。
整个过程分为两个阶段:
- 标记阶段:找到所有存活对象
- 清除阶段:清除所有垃圾对象
执行过程如图:
内存布局变化如图:
可以看到清除垃圾对象之后的内存是不连续的
优点:
- 相比于引用计数算法可以回收循环引用的对象
缺点:
- 容易导致内存空间碎片化
- 不会立即回收垃圾对象
标记整理
由于标记清除算法有一个比较大的问题,就是导致内存碎片,为了解决这个问题,在清除不可达对象之前,先做一下整理,使得活动对象占据的内存是连续的,这就是标记整理算法,是对标记清除算法的增强。标记整理阶段内存变化如下:
可以看到清除垃圾对象之后的内存是连续的
分代回收
分代回收算法并不是指特定的某一种GC回收算法,而是多种GC回收算法的组合算法,包括:
- 分代回收
- Scavenge 算法
- 标记清除
- 标记整理
- 增量标记
本文以V8引擎的垃圾回收机制进行讲解。
分代
首先分代回收算法会将内存对象分为新生代
和老生代
:
- 新生代:新生成的对象,存活时间较短的对象,例如局部变量
- 老生代:存活时间较长的对象,例如全局变量,闭包引用的对象,单例等
新生代GC
Scavenge算法
在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。而在Scavenge的具体实现中,主要采用了Cheney算法。
Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。
Scavenge的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。
由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模地应用到所有的垃圾回收中。但可以发现,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。
Scavenge算法的内存布局:
新生代GC流程如下:
老生代GC
对于老生代区域已经存在的对象主要是以标记清除方式为主,对于晋升过来的数据,先检查是否有足够空间,如果有则直接存储,如果没有足够空间,则进行标记管理,整理碎片内存,然后再进行存储
增量标记
V8引擎实际上在进行垃圾回收的时候是增量进行的,把一次回收过程分成了很多的子过程进行,这样可以避免因为垃圾回收导致的卡顿
下一篇:JS性能优化