JavaScript 中的内存管理是自动进行的,对我们来说是不可见的。我们创建基元、对象、函数……所有这些都需要内存。
当不再需要某样东西时会发生什么?JavaScript 引擎如何发现并清理它?
可达性
JavaScript 中内存管理的主要概念是可达性。
简单来说,“可达”的值是可以访问或以某种方式使用的值。它们保证存储在内存中。
-
有一组固有的可达值,由于明显的原因,它们无法被删除。
例如
- 当前执行的函数、其局部变量和参数。
- 当前嵌套调用链上的其他函数、其局部变量和参数。
- 全局变量。
- (还有一些其他内部变量)
这些值称为根。
-
如果可以通过引用或引用链从根访问到任何其他值,则该值被认为是可访问的。
例如,如果全局变量中有一个对象,并且该对象具有引用另一个对象的属性,则该对象被认为是可访问的。它引用的对象也是可访问的。后面有详细示例。
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”,是因为它在互联网上的文章涵盖得最好。对于其他引擎,许多方法是相似的,但垃圾回收在许多方面有所不同。
当您需要低级优化时,深入了解引擎是有好处的。在您熟悉该语言后,将此计划为下一步将是明智的。
评论
<code>
标记,对于多行 - 将它们包装在<pre>
标记中,对于超过 10 行 - 使用沙箱(plnkr,jsbin,codepen…)