myfreax

JavaScript垃圾收集:浏览器与服务器

我们都知道垃圾收集(GC)很重要对于现代应用程序的开发。 它依赖于你的编程语言,许多开发人员几乎不知道它是如何完成的

7 min read
By myfreax
JavaScript垃圾收集:浏览器与服务器

我们都知道垃圾收集(GC)很重要对于现代应用程序的开发。 它依赖于你的编程语言,许多开发人员几乎不知道它是如何完成的。

垃圾收集总是释放不再被使用的内存。 实现这一目标的策略和算法因某种语言而异。 例如,JavaScript通过一些有趣的方式,具体取决于您是否在浏览器或Node.js服务器上。

但你曾经考虑过这个过程如何在幕后工作? 让我们花点时间了解JavaScript GC在浏览器和服务器中的魔法。

内存周期

我们需要GC的原因是由于编程时的使用内存。 您创建函数,对象等等都会占用内存空间。

例如,与C相比,JavaScript的巨大优点是它是它为您自动执行内存分配。 这个过程非常简单,只需三个明确的步骤:

JavaScript Memory Lifecycle Visual
JavaScript的记忆生命周期。

对,但JavaScript在哪里存储了此数据? 基本上有两个目的地,第一个是内存堆 memory heap,第二个是堆栈stack。

堆是每个人都听说过的另一个术语。 它负责为我们动态内存分配。 换句话说,这个空间被保留用于JavaScript,以存储诸如对象和函数,而无需限制它可以使用的内存量。

与堆栈的一点不同,这是用于字面上堆叠元素的数据结构,例如原始数据指向真实对象的引用。 堆栈分配策略“更安全”,因为它知道分配了多少内存,且是固定的。

请看以下代码示例:

// heap and stack
const task = {
  name: 'Laundry',
  description: 'Call Mary to go with you...',
};

// stack
let name = 'Walk the dogs'; // 1
name = 'Walk; Feed the dogs'; // 2
const firstTask = name.slice(0, 5); // 3

每次在JavaScript中创建一个新对象时,堆内存中的空间都专用于它。

当涉及特殊情况(如使用不变值(如JavaScript中的原语)时,语言总是有利于使用先前的内存槽的重新分配。

以下是对上面的代码示例中的点1-3的解释:

  1. 简单地使用字符串值创建新的原始变量
  2. 用新的值一个覆盖其值。 发生这种情况时,JavaScript在stack上分配一个新的spot,而不是用当前stack
  3. 无论你这样做多少次,无论是通过直接分配还是方法返回的结果,JavaScript都会始终做同样的事情

JavaScript的垃圾收集算法

现在我们知道JavaScript如何处理内存分配以及分配时的内容。 但它是如何释放它们你的?

JavaScript的垃圾收集器会关心它,并且此过程如同听起来很简单:一旦对象不再使用,GC会释放其内存。

其实并不是那么简单,JavaScript如何知道哪些对象被收集。 这就需要算法场景的地方。

引用计数 GC

顾名思义,此策略通过在内存中搜索具有指向它们的零引用的资源。

让我们看之前的代码片段作为参考以获得更好的理解:

const task = {
  name: 'Laundry',
  description: 'Call Mary to go with you...',
};

task = 'Walk the dogs';

这是内部有多个属性task对象。 然后让我们假设另一个开发人员决定task可以简单地使用原语表示task = 'Walk the dogs'。 所以,第一个task对象不再指向它的引用,这使得它可用于GC。

这听起来很简单,但是实际情况远远不止于此。

有一个特殊的边缘案例您必须知道:循环依赖。 在之前,您可能从未想过它们,因为JavaScript也知道如何处理它们。 但通常情况下,他们以这种方式发生:

function task(n, d) {
    // ...

    reporter = { ... };
    assignee = { ... };

    reporter.assignee = assignee;
    assignee.reporter = reporter;
};

myTask = task('Laundry', 'Call Mary to go with you...');

这可能不会在真实应用程序中代表一个功能任务,但它足以想象两个对象的内部属性相互引用的情况。

这会产生一个循环。 一旦函数完成,JavaScript的参考计数GC将无法释放或者收集这两个对象,因为它们仍然彼此引用。

这是一个常见的场景,可以在真实的应用程序中轻松导致内存泄漏。 为避免这种情况,JavaScript向我们提供了第二种策略。

标记和扫描算法

标记和扫描算法以许多编程语言用于垃圾收集而闻名。 简而言之,它利用了智能方法来确定是否可以从根对象到达指定对象。

在JavaScript中,如果您在Node.js应用程序上,根对象是global对象; 如果您在浏览器上,它是window

算法从顶部开始,依次下降到层次结构,并依次标记可以到达的每个对象(即-仍然在存在引用的)并从根对象上清理扫描不到的那些对象。

node.js怎么释放内存

Node.js与Chrome一样都是基于V8 ,Google的开源JavaScript引擎。重要的一点是堆heap的分配

让我们来看看以下的表示:

Node New Space Old Space Comparison
New space vs. old space.

Node.js堆分为两个主要部分:新空间和旧空间。前者是分配新对象的地方,而后者是长期存放地方。

因此,新空间中对象的垃圾收集比旧空间更快。 平均而言,达到20%引用的对象,才能进入旧空间。

由于这一特点,V8利用了额外的GC策略:scavenger。

Scavenger

正如我们所见,Node以旧空间释放件事更昂贵。 当它必须这样做时,标记和扫描算法运行以实现目标。

scavenger GC专门从新生代收集垃圾。 其策略包括选择长期的对象并将其移动到所谓的旧空间。 为了实现这一步骤,V8确保至少一半的新生代仍然是空的; 否则,它会面临缺乏记忆的问题。

这个想法是跟踪所有引用到年轻一代的情况下,而无需通过整个旧一代。 此外,Scavenger还将一组引用从新空间中指向对象的旧空间。

结论

当然,这只是JavaScript宇宙中GC策略的概述。 这个过程更复杂,值得进一步阅读。 强烈推荐着名的Mozilla GC 文档和V8垃圾收集器作为补充资源。

请记住,与许多其他语言一样,我们无法确定GC运行。 自2019年以来,这取决于GC,不时地进行清理,您无法自己触发。

除此之外,您代码的方式非常影响javascript将分配多少内存。 这就是为什么了解垃圾收集器内存分配的特殊性以及释放内存的策略非常重要。 有几种开源lint和hint工具可以帮助您识别和分析这些泄漏以及代码中的其他陷阱。