Skip to content
On this page

垃圾回收机制

引用计数(Reference Counting)

  • 当一个对象被创建时,其引用计数器初始化为 1。
  • 当该对象被其他对象引用时,引用计数器加 1。
  • 当该对象不再被其他对象引用时,引用计数器减 1。
  • 当引用计数器减至 0 时,意味着该对象不再被引用,可以被垃圾收集器回收。
js
// 创建一个对象
let obj = { name: 'test' };
// 创建一个引用指向对象
let ref1 = obj; //引用计数+1 1

// 创建另一个引用指向对象
let ref2 = obj; //引用计数+1 2

// 引用失效
ref1 = null; //引用计数-1 1
ref2 = null; //引用计数-1 0

// 引用计数为0,对象可以被回收

当两个或多个对象相互引用时,它们的引用计数都不为零,即使它们已经不再被其他对象引用,也无法被回收。这导致内存泄漏。

js
const objA = {};
const objB = {};
//`objA`和`objB`相互引用,没有其他对象引用它们。
objA.ref = objB; //objA引用objB
objB.ref = objA; //objB引用objA

存在的问题:

循环引用:当两个或多个对象相互引用时,它们的引用计数都不为零,即使它们已经不再被其他对象引用,也无法被回收。这导致内存泄漏,因为这些对象仍然占据内存空间,却无法被释放。

标记-清除(Mark and Sweep)

定义:标记-清除(Mark and Sweep)算法通过标记不再使用的对象,然后清除这些对象的内存空间,以便后续的内存分配使用。

它分为两个阶段:标记阶段和清除阶段。

  • 标记阶段:
    • 在标记阶段,垃圾回收器会对内存中的所有对象进行遍历,从根对象开始(通常是全局对象)递归地遍历对象的引用关系。对于每个被访问到的对象,垃圾回收器会给它打上标记,表示该对象是可达的,即不是垃圾。这个过程确保了所有可达对象都会被标记。
  • 清除阶段:
    • 在清除阶段,垃圾回收器会遍历整个内存,对于没有标记的对象,即被判定为垃圾的对象,会被立即回收,释放内存空间。这样,只有被标记的对象会被保留在内存中,而垃圾对象会被清除。

在下面的图中,蓝色的元素代表被访问到的对象,即可达对象,灰色代表没有被访问到的对象,即不可达对象

优势:

  • 简单有效:标记-清除算法相对简单,容易实现。它可以准确地找到不再被引用的对象,并回收内存。
  • 处理循环引用:标记-清除算法能够处理循环引用的情况。当对象之间存在循环引用时,即使它们不再被任何其他对象引用,引用计数算法也无法将它们识别为垃圾,而标记-清除算法可以通过遍历的方式找到并清除这些对象。

存在的问题:

  • 垃圾回收过程中的停顿:标记-清除算法会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。
  • 内存碎片化:标记-清除算法会在回收过程中产生大量的不连续的、碎片化的内存空间。这可能导致后续的内存分配难以找到足够大的连续内存块,从而使得内存的利用率降低。

标记-整理(Mark and Compact)

定义:标记整理(Mark and Compact)可以看作是标记清除的增强操作,他在标记阶段的操作和标记清除一致,但是清除阶段会先执行整理,移动对象位置,对内存空间进行压缩。

它分为三个阶段:标记阶段、整理阶段和清除阶段。

  1. 标记阶段:将所有活动对象进行标记。
  2. 整理阶段:将内存中的活动对象移动到一端,使得空闲空间连续,并且没有碎片化。
  3. 清除阶段:将未标记的对象进行清除操作,并回收其占用的内存空间。

优势:

  • 解决了标记-清除算法的碎片化问题:标记-整理算法在清除阶段会将标记的对象整理到内存的一端,从而解决了标记-清除算法产生的碎片化问题。这样可以使得内存空间得到更好的利用,减少了空间的浪费。
  • 处理循环引用:标记-整理算法也能够处理循环引用的情况。

存在的问题:

  • 垃圾回收过程中的停顿:标记-整理算法同样会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。

V8 垃圾回收策略

为了提高垃圾回收的效率和性能,V8 引擎使用了分代式垃圾回收。分代式垃圾回收的基本思想是根据对象的存活时间将内存划分为不同的代(Generation),每一代都有不同的回收策略。根据统计数据,大部分对象的生命周期很短,而只有少部分对象会存活较长时间。因此,将内存按照对象的生命周期进行划分,可以更精确地对不同代的对象采取不同的回收策略,从而提高垃圾回收的效率和性能。

具体来说,V8 将内存划分为新生代(Young Generation)和老生代(Old Generation)两个代:

  • 新生代:存放的是存活时间较短的对象(经过一次垃圾回收后,就被释放回收掉),采用了基于 Scavenge 算法的快速垃圾回收策略,通过将内存分为两个半空间来进行垃圾回收,优化了对象的分配和回收过程。
  • 老生代:存放的是存活时间较长的对象(经过多次垃圾回收后仍存在),采用了基于标记-整理-清除算法的全垃圾回收策略,通过对整个堆进行标记和整理,以减少内存的碎片化,提高内存利用率。

通过采用分代式垃圾回收,V8 能够根据对象的生命周期进行针对性的优化,减少不必要的垃圾回收操作,提高垃圾回收的效率和性能,从而提升 JavaScript 的执行速度和用户体验。

新生代垃圾回收

  • 在 V8 引擎中,副垃圾回收器主要负责管理新生代的垃圾回收。
  • 新生代的垃圾回收是基于 Scavenge 算法的快速垃圾回收策略,而 Scavenge 算法的具体实现中,主要采用了一种基于复制的 Chenney 算法
  • 新生代的内存空间被划分为两个等大小的空间,分别称为 From 空间和 To 空间。

新对象首先被分配到 From 空间中,当 From 空间被占满时,就会触发垃圾回收机制。回收过程分为以下几个阶段:

  • 标记阶段:从根对象(通常是全局对象)开始,通过引用关系进行遍历并标记所有活动对象。
  • 复制阶段:将所有活动对象从 From 空间复制到 To 空间,并且进行排序,使得 To 空间成为连续的内存块。
  • 清除阶段:对 From 空间进行清理,回收非活动对象所占用的内存空间。
  • 空间交换:在清除阶段完成后,From 空间和 To 空间的角色会发生交换,即 From 空间变为 To 空间,To 空间变为 From 空间。这样,下一次的垃圾回收就可以在新的 To 空间中进行。

新生代对象晋升机制:

  • 年龄达到阈值:每个对象都有一个年龄计数器,初始为 0。每次经过一次垃圾回收,如果对象仍然存活,它的年龄计数器就会加 1。当年龄计数器达到阈值时,对象就会被晋升到老生代内存。
  • To 空间的内存占用达到一定比例:当 To 空间的内存占用超过一定比例(通常是 25%到 50%)时,也会触发对象的晋升。这是为了避免新生代内存过快地被填满,导致频繁的垃圾回收。

老生代垃圾回收

  • 在 V8 引擎中,主垃圾回收器主要负责管理老生代的垃圾回收。
  • 由于 Scavenge 算法在处理长时间存活和大规模对象存储时存在效率和内存利用率方面的不足,V8 引擎选择使用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)等算法来处理老生代的垃圾回收。这两种算法前面已经提到的,在标记清除的基础上将内存空间中产生大量不连续的内存碎片整理,使得内存空间连续。

Orinoco 优化

...

Released under the MIT License.