V8 垃圾回收机制浅析

1,393 阅读8分钟

V8 是一款由 Google 开发的高性能 JavaScript 引擎,用于解释执行 JavaScript 代码。在 JavaScript 中,由于无法手动管理内存,所以需要一种垃圾回收机制来自动释放不再使用的内存空间。

需要回收的‘垃圾’

  • 程序中不再使用的对象
function func() {
    name = 'lg'
    return `${name} is a coder`
}
func()
  • 程序中不能再访问的对象
function func() {
    const name = 'lg'
    return `${name} is a coder`
}
func()

常见的垃圾回收算法

垃圾回收算法是计算机科学中一种非常重要的技术,它主要用于自动管理内存。常见的垃圾回收算法包括以下几种:引用计数标记请除标记整理

引用计数

引用计数算法是一种垃圾回收算法,它基于对象的引用计数来判断对象是否还在使用中。具体来说,当一个对象被创建时,它的引用计数为 1,当它被另一个对象引用时,它的引用计数会增加 1,当一个对象的引用计数为 0 时,说明它已经没有被引用,可以被垃圾回收器回收。

引用计数算法的优点包括:

  1. 实时性:由于引用计数算法可以立即回收不再使用的对象,所以它可以及时释放内存,减少内存的浪费和碎片。
  2. 简单高效:引用计数算法实现简单,可以实现高效的内存回收。

但是引用计数算法也有一些缺点,包括:

  1. 循环引用问题:引用计数算法无法解决循环引用问题。当两个对象相互引用时,它们的引用计数永远不会为 0,导致它们永远不会被回收。
  2. 开销大:引用计数算法需要维护每个对象的引用计数,对于多线程和跨进程的应用来说,这个开销会非常大。

因此在现代的垃圾回收算法中已经很少使用了。

标记-清除(Mark-Sweep)算法

标记-清除(Mark-Sweep)算法是一种基于对象是否可达来进行垃圾回收的算法。具体来说,它将对象分为两类:可达对象和不可达对象。首先,它会从根对象开始遍历整个对象图,将所有可达对象标记为“存活”,然后扫描整个堆,将未被标记的对象清除掉。

image.png

标记-清除算法的优点包括:

  1. 不会产生内存碎片:标记-清除算法不会移动对象,因此不会产生内存碎片。
  2. 相对简单:标记-清除算法相对于其他算法来说比较简单。

image.png

但是标记-清除算法也有一些缺点,包括:

  1. 效率低:标记-清除算法需要对整个堆进行扫描,对于大型对象或大量对象的情况,垃圾回收的效率会非常低。
  2. 空间利用率低:标记-清除算法可能会出现内存碎片,导致空间利用率低。
  3. 暂停时间长:在执行标记和清除过程中,垃圾回收器会停止应用程序,这会导致暂停时间较长,影响应用程序的响应性。

综上所述,标记-清除算法在一些场景下具有一定的优势,例如对于生命周期较长的对象或需要保持对象顺序的场景。但是对于大量对象或大型对象的场景,它的效率会比较低,并且可能会产生内存碎片。因此在实际应用中,一般会结合其他垃圾回收算法来使用。

标记整理算法

它是标记请除算法的增强版本。通过将存活的对象向堆的一端移动,然后清除掉堆尾的所有对象来实现垃圾回收。具体来说,标记整理算法会将所有存活对象标记,并将它们移动到堆的一端,然后将堆端以外的所有对象清除掉。

image.png

image.png

image.png

标记整理算法的优点包括:

  1. 不会产生内存碎片:与标记-清除算法不同,标记整理算法可以将存活对象移动到一端,然后将堆端以外的所有对象清除掉,因此不会产生内存碎片。
  2. 效率较高:标记整理算法在执行时只需要对存活对象进行移动和清除操作,相对于标记-清除算法来说,效率会更高。
  3. 空间利用率较高:标记整理算法可以使得存活对象紧凑地排列在一起,从而提高空间利用率。

但是标记整理算法也有一些缺点,包括:

  1. 需要移动对象:标记整理算法需要将存活对象移动到一端,这会导致一定的开销。
  2. 暂停时间长:在执行标记和整理过程中,垃圾回收器会停止应用程序,这会导致暂停时间较长,影响应用程序的响应性。

V8 垃圾回收

V8 引擎采用了现代的、基于内存分代的垃圾回收机制,它将内存分为两个部分:新生代和老生代。

新生代垃圾回收 image.png

新生代垃圾回收主要处理新创建的对象,这些对象往往很快就会变得不再使用。V8 引擎使用了 复制算法与标记整理算法来实现新生代垃圾回收。将新生代内存空间分为两个区域:From 空间和 To 空间。当 From 空间被填满时,V8 就会启动垃圾回收过程,将 From 空间中的存活对象复制到 To 空间中,然后清空 From 空间。最后,From 空间和 To 空间交换,To 空间变成新的 From 空间。这样,新生代垃圾回收就完成了。

老生代对象 老生代垃圾回收通常比新生代垃圾回收要复杂。V8 引擎采用了两种不同的算法来实现老生代垃圾回收:标记请除和标记整理算法

V8 引擎首先将所有对象标记为未标记。然后,从根对象(如全局变量、当前执行的函数、执行栈等)开始遍历所有对象,将能够访问到的对象标记为已标记。最后,V8 引擎清除所有未标记的对象,并将它们的内存空间加入空闲链表中,以便下次分配对象时使用。

在 标记整理算法中,V8 引擎首先将所有对象标记为未标记。然后,从根对象开始遍历所有对象,将能够访问到的对象标记为已标记。接着,V8 引擎将所有已标记的对象移动到内存的一端,从而在内存中创建了一个连续的区域。最后,V8 引擎清除所有未标记的对象,并将它们的内存空间加入空闲链表中,以便下次分配对象时使用。

V8 引擎采用了许多优化措施来提高老生代垃圾回收的效率。其中一个重要的优化措施是增量标记。在增量标记中,V8 引擎会将垃圾回收过程分成多个阶段,每个阶段只标记一部分对象。这样,垃圾回收的过程可以与应用程序同时进行,避免了长时间的阻塞。

image.png

另一个优化措施是并发标记。在并发标记中,V8 引擎将标记对象的任务交给后台线程进行处理,同时应用程序可以继续执行。这样,垃圾回收过程不会阻塞应用程序的执行,从而提高了整体的性能。

内存限制和内存泄漏

V8 引擎中的内存限制是通过两个参数进行控制的:--max-old-space-size 和 --max-new-space-size。--max-old-space-size 参数控制老生代区域的最大内存大小,--max-new-space-size 参数控制新生代区域的最大内存大小。当内存占用超过这些限制时,V8 引擎就会触发垃圾回收。

内存泄漏是一种常见的问题,它可能导致应用程序占用过多的内存,从而降低系统的性能。在 V8 引擎中,内存泄漏通常是由于对全局变量、闭包等长期存在的对象的引用没有被正确地释放导致的。为了避免内存泄漏,开发人员需要注意对对象的引用计数,及时释放不再使用的对象。

总结

V8 引擎采用了先进的垃圾回收机制来管理内存。新生代垃圾回收和老生代垃圾回收都采用了不同的算法,可以针对不同类型的对象进行处理。在垃圾回收过程中,V8 引擎采用了许多优化措施来提高性能,如增量标记、并发标记等。同时,开发人员需要注意内存限制和内存泄漏问题,以保证应用程序的性能和稳定性。