V8 引擎中的垃圾收集器
垃圾收集是跟踪活动对象的过程,同时销毁堆内存中未引用的对象,为将来创建的新对象腾出空间。本文将讨论V8
引擎的垃圾收集器,V8
引擎是由 Google
用 C++
编写开发的开源、高性能 JavaScript
和 WebAssembly
引擎。
V8 引擎用于各种应用程序,例如 Chrome 和 Node.js。
V8
垃圾收集器有一些必须定期执行的基本任务:
- 识别活动对象(被引用)。
- 回收/重用死对象(未被引用)占用的内存。
- 压缩/碎片内存。(取决于
GC
的场景)
V8
堆分为两个主要组:年轻代,进一步分为幼儿代和中间子代,以及老一代。当对象第一次创建时,它们被放置在幼儿代中。如果它们在下一次垃圾回收中幸存下来,它们就会被移动到年轻代的中间子代。如果它们在另一次垃圾回收中幸存下来,它们就会被移入老一代。
每一代都有不同的算法来清理垃圾。
- 年轻代 ——
Minor GC
- 老年代 ——
Major GC
在解释这两种算法如何工作之前,有一个非常重要的假设需要写出来。
世代假设:婴儿死亡率或世代假设是这样的观察:在大多数情况下,年轻的物体比旧的物体更有可能死亡。
这基本上表明大多数物体都会在年轻时死亡。从垃圾收集的角度来看,大多数对象被分配后几乎立即变得不可访问。对于大多数动态语言来说都是如此。
主要垃圾收集器 - Major GC
Major GC
从整个堆中收集垃圾。
标记(Marking)
标记过程涉及识别可到达的对象。垃圾收集器从一组已知的对象指针(称为根)开始,其中包括执行堆栈和全局对象。然后,它跟随每个指针指向下一个可到达的对象,并将其标记为这样。此过程递归地继续,直到找到并标记运行时中的每个可到达的对象。
清理(Sweeping)
清理是通过将死对象添加到称为“空闲列表”的数据结构来识别和填充死对象留下的内存间隙的过程。一旦垃圾收集器完成标记不可访问的对象,它就会找到连续的间隙并将它们添加到“空闲列表”中以便于访问。当我们需要分配内存时,我们可以快速检查Free List
数据结构来找到合适的可用内存块。
压缩(Compaction)
在某些情况下,垃圾收集器使用碎片启发式来决定要压缩哪些页面。然后,使用该页面的“空闲列表”将幸存的对象复制到未压缩的页面中。通过这样做,我们能够重用死亡对象留下的小而分散的内存间隙。
小型垃圾收集器 - Minor GC
Minor GC
使用Scavenger
算法收集年轻代中的垃圾。
正如“年轻一代和老一代结构”的图像所示,有幼儿代和中间子代区域。我们将这两个区域分为From-Space
(幼儿代)和To-Space
(中间子代)。
在分配对象期间,它们被放置在From-Space
中,当Scavenger
算法启动时,对象被移动到To-Space
。在最坏的情况下,每个对象都可以在清理阶段幸存下来,这需要复制每个对象,这意味着总空间的一半始终是空的。
为了进行清理,我们有一组额外的根,它们是从旧到新的参考。这些是旧空间中的指针,指向年轻代中的对象。这些与堆栈和全局变量相结合,而不是跟踪每次清理的整个堆图,这就是我们如何知道年轻代的每个引用,而不必跟踪整个老年代。
scavenger
步骤将所有幸存的对象移动到连续的内存块(死对象留下的间隙被活对象和内存字节覆盖)。然后我们切换两个空间,即从From-Space
到To-Space
,反之亦然。一旦垃圾收集完成,新的分配就会发生在起始空间中的下一个空闲地址。
仅凭这一策略,我们很快就会耗尽年轻一代的空间。在第二次垃圾回收中幸存下来的对象将被移至老一代而不是 To-Space
中。然后,我们更新引用已移动的原始对象的指针。每个复制的对象都会留下一个转发地址,用于更新原始指针以指向新位置。
在清理过程中,发生了三件事。标记、移动和指针更新。全部一起。
从操作系统的角度来看算法如何工作
scavenger(Minor GC)
在年轻代垃圾收集过程中,V8
使用并行清理在多个辅助线程之间分配工作。每个线程都被分配了一定数量的指针来跟踪并急切地将它遇到的任何活动对象疏散到To-Space
中。但是,在尝试清空对象时,清理任务必须通过原子读/写/比较和交换操作相互协调。
Full Mark Compact (Major GC)
在老年代垃圾收集过程中,V8
从并发标记开始。当堆接近动态计算的限制时,将启动并发标记任务。每个助手都被分配了许多要跟踪的指针,它们在跟踪发现的对象的所有引用时标记遇到的每个对象。当 JavaScript
在主线程上执行时,并发标记在后台进行。
一旦并发标记完成或达到动态分配限制,主线程就会执行快速标记完成步骤。这会导致主线程暂停,这个暂停代表了Major GC
的总暂停时间。然后主线程再次扫描根以确保所有活动对象都被标记,然后在几个助手的帮助下开始并行压缩和指针更新。并非旧空间中的所有页面都可以被压缩,因此不符合条件的页面将使用前面提到的空闲列表进行清理。在暂停期间,主线程还会启动并发清理任务,这些任务与并行压缩任务和主线程本身同时运行。即使 JavaScript
在主线程上运行,这些任务也可以继续。
总结
在本文中,讨论了 V8
垃圾收集器的工作原理以及它为何如此工作。还解释了堆被分为 Minor GC
和 Major GC
两个子类的原因,以及每种垃圾收集的算法如何发挥作用。此外,还从操作系统及其线程的角度描述了这些进程如何运行。