测验

JavaScript 垃圾回收机制是如何工作的?

主题
JavaScript
在GitHub上编辑

TL;DR

JavaScript 中的垃圾回收是一种自动内存管理机制,用于回收程序不再使用的对象和变量所占用的内存。两种最常见的算法是标记-清除和分代垃圾回收。

标记-清除

JavaScript 中使用最常见的垃圾回收算法是标记-清除算法。它分两个阶段运行:

  • 标记阶段:垃圾回收器遍历对象图,从根对象(全局变量、当前执行的函数等)开始,将所有可访问的对象标记为“在使用中”。
  • 清除阶段:垃圾回收器扫描内存,删除所有未标记的对象,因为它们被认为不可访问且不再需要。

该算法有效地识别并删除已变得不可访问的对象,从而为新的分配释放内存。

分代垃圾回收

现代 JavaScript 引擎利用了这一点,对象根据其年龄和使用模式被划分为不同的代。经常访问的对象被移动到年轻代,而较少使用的对象被提升到老年代。这种优化通过专注于年轻代(大多数对象寿命短)来减少垃圾回收的开销。

不同的 JavaScript 引擎(根据浏览器而异)实现不同的垃圾回收算法,并且没有标准的垃圾回收方式。


JavaScript 中的垃圾回收

JavaScript 中的垃圾回收是由 JavaScript 引擎管理的自动过程,旨在回收不再需要的对象所占用的内存。这有助于防止内存泄漏并优化可用内存的使用。以下是 JavaScript 中垃圾回收工作原理的概述:

内存管理基础

JavaScript 在创建对象、数组和其他变量时为其分配内存。随着时间的推移,其中一些对象变得不可访问,因为没有对它们的引用。垃圾回收是识别这些不可访问的对象并回收其内存的过程。

可达性

JavaScript 垃圾回收中的主要概念是可达性。如果一个对象可以通过某种方式访问或到达,则认为它是可达的:

  • 全局变量:全局变量引用的对象始终是可达的。
  • 局部变量和函数参数:只要函数正在执行,这些对象就是可达的。
  • 闭包变量:如果闭包是可达的,则闭包引用的对象是可达的。
  • DOM 和其他系统根:DOM 或其他宿主对象引用的对象。

如果从根到对象存在引用链,则该对象被认为是可达的。

垃圾回收算法

  1. 标记和清除
    • 标记阶段:垃圾收集器从根对象开始,标记所有可访问的对象。
    • 清除阶段:然后扫描内存,查找未被标记的对象,并回收它们的内存。
  2. 引用计数
    • 此算法保留对每个对象的引用计数。当对象的引用计数降为零时,它被认为不可访问,可以被收集。
    • 引用计数的缺点是它不能很好地处理循环引用(例如,两个对象相互引用,但没有被任何其他对象引用)。
  3. 分代垃圾回收
    • 内存被划分为几代:年轻代和老年代。
    • 对象最初分配在年轻代中。
    • 存活了多次收集的对象被提升到老年代。
    • 年轻代收集更频繁且更快,而老年代收集不太频繁,但涵盖更多对象。

JavaScript 引擎实现

不同的 JavaScript 引擎使用这些算法的变体:

  • V8 (Google Chrome, Node.js):使用分代、标记和清除以及其他优化来高效地进行垃圾回收。
  • SpiderMonkey (Mozilla Firefox):使用增量和分代垃圾回收。
  • JavaScriptCore (Safari):使用带有分代收集的标记和清除算法。

内存泄漏

当程序未能释放它不再需要的内存时,JavaScript 中就会发生内存泄漏,导致程序随着时间的推移消耗越来越多的内存。

JavaScript 中的内存泄漏可能由于各种原因而发生,包括:

  • 意外的全局变量:无意中创建全局变量,即使不再需要它们,这些变量仍然保留在内存中。
  • 闭包:不正确地使用闭包,其中内部函数保留对来自外部函数作用域的变量的引用,阻止外部函数的作用域被垃圾回收。
  • 事件监听器:当不再需要事件监听器或回调时,未能删除它们,导致相关对象保留在内存中。
  • 缓存:实现缓存时没有适当的逐出逻辑,导致随着时间的推移内存无界增长。
  • 分离的 DOM 节点引用:保留对分离的 DOM 节点的引用,阻止它们被垃圾回收。
  • 被遗忘的计时器或回调:未能清除计时器或回调,导致它们的相关数据保留在内存中。

为了避免内存泄漏:

  • 删除事件监听器:当不再需要事件监听器时,始终删除它们。
  • 清除闭包中的引用:避免在闭包中保留不必要的引用。
  • 管理 DOM 引用:当不再需要 DOM 节点及其引用时,显式删除它们。
  • 避免全局变量:尽量减少全局变量的使用,以降低无意中保持引用存活的风险。

延伸阅读

在GitHub上编辑