一、什么是垃圾回收(GC)
许多现代语言引擎(例如 Chrome 的 V8 JavaScript 引擎)都会动态管理正在运行的应用程序的内存,因此开发人员无需自己担心。引擎会定期移交分配给应用程序的内存,确定哪些数据不再需要,然后清除它们以释放空间。此过程称为垃圾收集,下文称 GC。
垃圾回收的目的是释放那些程序不要要的内存区域,以便后续再次分配,保证程序稳定运行。
二、垃圾回收器
GC 程序中用于进行垃圾回收的模块成为垃圾回收器,垃圾回收器在垃圾回收时需要做的事情如下:
- 识别存活对象、失活(垃圾)对象
- 回收垃圾对象所占的内存空间
- 内存碎片整理(也叫做压缩)(可选)
三、标记清除
标记清除是一种垃圾回收方式,其操作方式是从一组已知的根对象(称之为“根集”)递归查找引用,标记可访问对象,对于未被标记的对象会在 GC 过程中被清除。
标记: 垃圾回收的一个最重要的步骤首先是识别 "垃圾",垃圾回收器利用对象的可达性来得知对象是否活跃。
垃圾回收器从一组已知的对象指针开始,称为“根集”,包括执行堆栈、全局对象。执行堆栈中的数据还正在被运行中的程序使用,因此需要保留,还有它引用的其他数据。全局对象的数据一直存活,因此也需要保留。
从根集出发递归找到所有可到达的引用,并将这些对象标记为可访问。没有被标记的对象将成为垃圾。
清除 清除步骤主要是清理在标记阶段没有被标记为可访问的"垃圾"对象。
压缩: 由与清理后的内存有些可能是不连续的、零碎的空间,这对与后面我们继续分配内存不大友好。我们希望的是在清理掉垃圾对象之后可以通过移动一些对象在内存中的位置来达到减少内存中零碎空间的目的。因此我们可以通过复制一些对象来整理内存空间。
但这并不总是好的,因为在老生代内存中复制大量的对象成本很高,所以在整理内存的时候可以选择那些零碎程度比较高的内存区域进行整理,以此来达到一种权衡。
四、分代垃圾回收策略
分代垃圾回收是在内存管理方面,将内存分为 "新生代区域" 与 "老生代区域"。两种区域可以各自执行不同的垃圾回收策略,这样做的目的是提高垃圾回收的效率,下面会展开讲解。
新生代区域:程序分配的对象首先会被分配到新生代区域,根据 "分代假说" ,我们认为大多数新分配的对象在很短时间内便会成为垃圾。新生代的内存区域较小,更加容易触发垃圾回收。
老生代区域:在新生代中经历了垃圾回收之后依然存活的对象(或者是那些很大的对象)将会被提升到老生代内存区。老生代的内存区域较大,主要存放一些寿命长、占用空间大的对象,触发的频率也较低。
新生代的垃圾回收(Minor GC)
由于新生代区域中对象具有分配快、寿命短的特点,所以新生代的 GC 频繁。
新生代区域采用标记清除来进行 GC。在清除阶段使用半空间复制法来进行
内存的清理。
半空间复制法是将内存分为两个对等的区域,每次垃圾回收时把存活的对象从一半区域(From)复制到另一半(To)区域,然后清空 From 区域中的对象。这样做的好处有:
- 减少内存碎片的产生,由于存活的对象是从
From复制到To, 所以在复制时可以向内存区域的一端紧凑排列。这样不会产生内存碎片(见下图 4-1)。 - 优化对象访问,由于半空间复制策略将存活对象复制到新的空间并保持内存连续,这有助于提高对象访问的局部性。内存局部性好意味着对象更有可能位于相邻的内存位置,从而提高缓存命中率和整体性能。
图 4-1
老生代垃圾回收(Major GC)
由于老生代区域中的对象具有稳定、空间大的特点,所以老生代的内存回收效率更低,频率也更低。
老生代区域同样采用标记清除来进行 GC。在清除阶段,老生代没有使用像新生代那样的半空间复制算法。因为对于老生代区域,里面的对象由于较为稳定,所以在进行垃圾回收的时候一般存活的对象较多。采用半空间算法一是空间利用率降低,有效空间变为原来的一半。二是复制存活对象的成本较大。所以老生代区域的清除阶段是直接对不可访问的对象进行内存清理。
五、优化
垃圾回收器的执行在主线程上,这可能会导致卡顿,尤其是当应用程序较大时,Major GC 的标记阶段可能达到 100 ms ,这严重阻塞了主线程。为了避免这一情况,V8 在 GC 方面做了很多优化。
增量标记
垃圾回收可以间歇性工作,不会一次性执行整个 GC ,而是执行一小部分,这样可以让给主线程来执行其他工作,从而避免主线程阻塞时间过长
并行
引入辅助线程执行 GC,主线程可以执行其他工作。
还有一些其他的优化方式不再一一列举,感兴趣可以自行查阅资料查看,总之 V8 的优化超乎我们想象!
六、弱引用与垃圾回收
JS 中有一些数据结构如 WeakMap、WeakSet,其键值是一个对象(假设为 obj),WeakMap 对象对 obj 的引用是一个弱引用,也就是说如果对象 obj 除了 WeakMap 对象对其的引用外没有其他可达引用。那么垃圾回收器会把 obj 视为垃圾回收掉。
考虑下面例子:
// > global.gc(); 先进行一次垃圾回收
const objKey = {
name:'objKey'
}
window.objKey = objKey // 全局对象对 objKey 的引用
const weakMap = new WeakMap()
weakMap.set(objKey,true)
// > global.gc();
// 此时由于 objKey 有来自于全局对象 window.objKey 的引用,所以不会被垃圾回收
window.objKey = null // 清除 window.objKey 对对象 objKey 的引用
// > global.gc();
// 这里由于 objKey 对象的可达引用 window.objKey 被清除,只剩下 weakMap 对 objKey 的引用,但这个引用时弱引用,所以垃圾回收时依然会把 objKey 这个对象回收。
七、总结
垃圾回收是确保程序稳定运行的重要机制,V8 引擎通过分代垃圾回收策略、半空间复制算法和多种优化措施,提高了垃圾回收的效率和性能。了解垃圾回收的原理和优化策略,对于开发高效、稳定的应用程序至关重要。
同时,了解 V8 的垃圾回收机制能加深我们 V8 引擎的认识,同时在开发中也能更加关注变量的分配与回收。无论在开发还是学习上都能带来不少好处。