2022 年 10 月 14 日

垃圾回收

JavaScript 中的内存管理是自动进行的,对我们来说是不可见的。我们创建基元、对象、函数……所有这些都需要内存。

当不再需要某样东西时会发生什么?JavaScript 引擎如何发现并清理它?

可达性

JavaScript 中内存管理的主要概念是可达性

简单来说,“可达”的值是可以访问或以某种方式使用的值。它们保证存储在内存中。

  1. 有一组固有的可达值,由于明显的原因,它们无法被删除。

    例如

    • 当前执行的函数、其局部变量和参数。
    • 当前嵌套调用链上的其他函数、其局部变量和参数。
    • 全局变量。
    • (还有一些其他内部变量)

    这些值称为

  2. 如果可以通过引用或引用链从根访问到任何其他值,则该值被认为是可访问的。

    例如,如果全局变量中有一个对象,并且该对象具有引用另一个对象的属性,则对象被认为是可访问的。它引用的对象也是可访问的。后面有详细示例。

JavaScript 引擎中有一个称为垃圾回收器的后台进程。它监视所有对象,并删除那些已变得不可访问的对象。

一个简单的示例

下面是最简单的示例

// user has a reference to the object
let user = {
  name: "John"
};

此处箭头描述了一个对象引用。全局变量 "user" 引用对象 {name: "John"}(为了简洁,我们称其为 John)。John 的 "name" 属性存储一个基元,因此它绘制在对象内部。

如果 user 的值被覆盖,则引用将丢失

user = null;

现在 John 变得不可访问。无法访问它,没有对其的引用。垃圾回收器将清除数据并释放内存。

两个引用

现在让我们想象一下,我们将引用从 user 复制到 admin

// user has a reference to the object
let user = {
  name: "John"
};

let admin = user;

现在,如果我们执行相同的操作

user = null;

…那么该对象仍然可以通过 admin 全局变量访问,因此它必须保留在内存中。如果我们也覆盖 admin,则可以将其删除。

相互链接的对象

现在是一个更复杂的示例。家庭

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

函数 marry 通过相互提供引用来“结婚”两个对象,并返回一个包含这两个对象的新对象。

生成的内存结构

截至目前,所有对象都是可访问的。

现在让我们删除两个引用

delete family.father;
delete family.mother.husband;

仅删除这两个引用中的一个还不够,因为所有对象仍然是可访问的。

但是,如果我们删除这两个引用,那么我们可以看到 John 没有传入引用了

传出引用无关紧要。只有传入引用才能使对象可访问。因此,John 现在不可访问,并将连同所有也变得不可访问的数据一起从内存中删除。

垃圾回收后

不可访问的孤岛

整个相互链接的对象孤岛都可能变得不可访问,并从内存中删除。

源对象与上述相同。然后

family = null;

内存中的图片变为

此示例演示了可达性概念的重要性。

显然,John 和 Ann 仍然链接,两者都有传入引用。但这还不够。

之前的 "family" 对象已从根部取消链接,不再引用它,因此整个岛屿变得不可达,并将被移除。

内部算法

基本垃圾回收算法称为“标记清除”。

定期执行以下“垃圾回收”步骤

  • 垃圾回收器获取根并“标记”(记住)它们。
  • 然后它访问并“标记”来自它们的全部引用。
  • 然后它访问标记的对象并标记它们的引用。记住所有访问的对象,以便将来不再访问同一对象两次。
  • …以此类推,直到访问了从根部可达的每个引用。
  • 移除除标记对象之外的所有对象。

例如,我们的对象结构如下所示

我们清楚地看到右侧有一个“不可达岛屿”。现在让我们看看“标记清除”垃圾回收器如何处理它。

第一步标记根部

然后我们遵循它们的引用并标记引用的对象

…并在可能的情况下继续遵循进一步的引用

现在,在此过程中无法访问的对象被视为不可达,并将被移除

我们还可以将这个过程想象成从根部洒出一大桶油漆,它流经所有引用并标记所有可达对象。然后移除未标记的对象。

这就是垃圾回收的工作原理。JavaScript 引擎应用了许多优化,使其运行得更快,并且不会给代码执行带来任何延迟。

一些优化

  • 代际回收 – 对象分为两组:“新对象”和“旧对象”。在典型代码中,许多对象的生命周期很短:它们出现、完成工作并快速死亡,因此跟踪新对象并在这种情况下清除内存是有意义的。那些存活足够长久的对象会变成“旧对象”,并且检查频率较低。
  • 增量回收 – 如果有很多对象,并且我们尝试一次遍历并标记整个对象集,则可能需要一些时间并在执行中引入明显的延迟。因此,引擎将现有对象集的整体拆分为多个部分。然后逐个清除这些部分。有很多小型垃圾回收,而不是一次性全部回收。这需要在它们之间进行一些额外的记账以跟踪更改,但我们得到许多微小的延迟,而不是一个大的延迟。
  • 空闲时间回收 – 垃圾回收器仅在 CPU 空闲时尝试运行,以减少对执行的可能影响。

还有其他优化和垃圾回收算法。尽管我很想在这里描述它们,但我不得不忍住,因为不同的引擎实现了不同的调整和技术。而且,更重要的是,随着引擎的发展,事情会发生变化,因此在没有实际需要的情况下深入“预先”学习可能不值得。当然,除非这是纯粹兴趣的问题,那么下面会有一些链接供你参考。

总结

需要了解的主要内容

  • 垃圾回收自动执行。我们无法强制或阻止它。
  • 对象在可达时保留在内存中。
  • 被引用与可达(从根部)不同:一组相互链接的对象可能整体上变得不可达,正如我们在上面的示例中看到的。

现代引擎实现了先进的垃圾回收算法。

一本通用的书“垃圾回收手册:自动内存管理的艺术”(R. Jones 等)涵盖了其中一些。

如果您熟悉低级编程,有关 V8 垃圾回收器的更详细信息可以在文章 V8 之旅:垃圾回收 中找到。

V8 博客 也时不时发表有关内存管理变化的文章。当然,要了解更多有关垃圾回收的信息,您最好先准备学习 V8 内部结构,并阅读担任 V8 工程师之一的 Vyacheslav Egorov 的博客。我说:“V8”,是因为它在互联网上的文章涵盖得最好。对于其他引擎,许多方法是相似的,但垃圾回收在许多方面有所不同。

当您需要低级优化时,深入了解引擎是有好处的。在您熟悉该语言后,将此计划为下一步将是明智的。

教程地图

评论

在评论之前阅读此内容...
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于超过 10 行 - 使用沙箱(plnkrjsbincodepen…)