跳到主要内容

V8引擎的垃圾回收机制

V8 垃圾回收策略

自动垃圾回收有很多算法,由于不同对象的生存周期不同,所以无法只用一种回收策略来解决问题,这样效率会很低。

V8 采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)和老生代(old generation)。

新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升

分代内存

默认情况下,32 位系统新生代内存大小为 16MB,老生代内存大小为 700MB,64 位系统下,新生代内存大小为 32MB,老生代内存大小为 1.4GB。

新生代平均分成两块相等的内存空间,叫做 semispace,每块内存大小 8MB(32 位)或 16MB(64 位)。

新生代

分配方式

新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就可以了,当存储空间快要满时,就进行一次垃圾回收。

算法

新生代采用 Scavenge 垃圾回收算法,在算法实现时主要采用 Cheney 算法。

Cheney 算法将内存一分为二,叫做 semispace,一块处于使用状态,一块处于闲置状态。

处于使用状态的 semispace 称为 From 空间,处于闲置状态的 semispace 称为 To 空间。 Scavenge GC 算法:

  • 当 from 空间被占满时,启动 GC 算法
  • 存活的对象从 from space 转移到 to space
  • 清空 from space
  • from space 与 to space 互换
  • 完成一次新生代 GC

总结一下,把活跃的复制到另一边,清除原来的,然后颠倒form和to的指向,保证了to空间在一次算法后始终是空的,留来复制from中活跃的元素

晋升

当一个对象经过多次复制仍然存活时,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

对象从新生代移动到老生代的过程叫作晋升。

从 新生代空间 转移到 老生代空间 的条件:

  • 经历过一次以上 Scavenge GC 的对象:如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
  • 当要从From空间复制一个对象到To空间时, 当 to 空间的体积超过 25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

老生代

老生代占用内存较多(64 位为 1.4GB,32 位为 700MB),如果使用 Scavenge 算法,浪费一半空间不说,复制如此大块的内存消耗时间将会相当长。所以 Scavenge 算法显然不适合。 所以,V8 在老生代中主要采用了 Mark-Sweep(标记清除)和 Mark-Compact(标记整理)相结合的方式进行垃圾回收。

Mark-Sweep(标记清除)

标记清除分为标记和清除两个阶段。在标记阶段需要遍历堆中的所有对象,并标记那些活着的对象,然后进入清除阶段。在清除阶段总,只清除没有被标记的对象。由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高

标记清除有一个问题就是进行一次标记清楚后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,将会使得 V8 无法完成这次分配,提前触发垃圾回收。

标记优化策略:

  • 增量标记: 小模块标记,在代码执行间隙执,GC 会影响性能
  • 并发标记(最新技术): 不阻塞 js 执行

Mark-Compact(标记整理)

标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。

在整理的过程中,将活着的对象向内存区的一端移动,移动完成后直接清理掉活跃边界外的内存。紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片

全局对象的回收

定义一个全局对象不会被回收,你需要对引用改对象的变量执行delete或者重新赋值,堆中的对象没有引用关系了才会在下次gc中回收

先了解这么多以后慢慢消化