避免 NodeJS 中的内存泄漏:性能最佳实践

2025-06-04

避免 NodeJS 中的内存泄漏:性能最佳实践

内存泄漏是每个开发人员最终都会遇到的问题。即使语言自动管理内存,内存泄漏在大多数语言中也很常见。内存泄漏可能导致应用程序速度变慢、崩溃、延迟过高等等问题。

在本文中,我们将探讨什么是内存泄漏以及如何在 NodeJS 应用程序中避免内存泄漏。虽然本文主要关注 NodeJS,但通常也适用于 JavaScript 和 TypeScript。避免内存泄漏有助于应用程序高效利用资源,并带来性能提升。

我们很高兴地宣布,AppSignal for Node.js 的首个版本已于近期发布。它集成了 Express 功能,并包含一个用于自动检测 Node.js 模块的接口。更多详情,请参阅我们的文档

JavaScript 中的内存管理

要理解内存泄漏,我们首先需要了解 NodeJS 中内存的管理方式。这意味着要了解 NodeJS 使用的 JavaScript 引擎如何管理内存。NodeJS 使用V8 引擎来开发 JavaScript。您可以查看V8 引擎中的内存管理可视化,以更好地理解 V8 引擎中 JavaScript 内存的构造和使用方式。

让我们简要回顾一下上述文章:

内存主要分为栈内存和堆内存。

  • :静态数据(包括方法/函数框架、原始值以及指向对象的指针)存储在栈中。该空间由操作系统 (OS) 管理。
  • :这是 V8 存储对象或动态数据的地方。这是最大的一块内存区域,也是垃圾收集(GC)发生的地方。

V8 通过垃圾回收机制来管理堆内存。简单来说,它会释放孤儿对象(即不再被栈直接或间接(通过其他对象中的引用)引用的对象)占用的内存,以便为新对象的创建腾出空间。

V8 中的垃圾收集器负责回收未使用的内存,以供 V8 进程重用。V8 垃圾收集器是分代的(堆中的对象按其年龄分组,并在不同阶段清除)。V8 的垃圾收集分为两个阶段,使用三种不同的算法。

标记-清除-压缩 GC

什么是内存泄漏

简单来说,内存泄漏就是堆上一个孤立的内存块,应用程序不再使用,垃圾回收器也没有将其归还给操作系统。所以实际上,它是一个无用的内存块。随着时间的推移,这样的内存块累积起来可能会导致应用程序没有足够的内存可用,甚至操作系统也没有足够的内存可供分配,从而导致应用程序甚至操作系统的运行速度变慢甚至崩溃。

JS 中内存泄漏的原因

V8 中的自动内存管理(例如垃圾回收)旨在避免此类内存泄漏。例如,循环引用不再是问题,但由于堆中不必要的引用,仍然可能引发此类泄漏,并且可能由各种原因造成。下面列出了一些最常见的原因。

  • 全局变量:由于 JavaScript 中的全局变量由根节点(window 或 global this)引用,因此它们在应用程序的整个生命周期内都不会被垃圾回收,并且只要应用程序运行,它们就会占用内存。这适用于全局变量引用的任何对象及其所有子对象。从根节点引用大量对象可能会导致内存泄漏。
  • 多重引用:当同一个对象被多个对象引用时,如果其中一个引用悬空,则可能会导致内存泄漏。
  • 闭包:JavaScript 闭包有一个很酷的特性,那就是能够记住其周围的上下文。当闭包持有对堆中某个大型对象的引用时,只要闭包正在使用,它就会将该对象保留在内存中。这意味着,持有此类引用的闭包很容易被不当使用,从而导致内存泄漏。
  • 计时器和事件:当在回调中保留大量对象引用而没有进行适当处理时,使用 setTimeout、setInterval、观察者和事件监听器可能会导致内存泄漏。

避免内存泄漏的最佳实践

现在我们了解了导致内存泄漏的原因,让我们看看如何避免内存泄漏以及如何使用最佳实践来确保高效使用内存。

减少全局变量的使用

由于全局变量永远不会被垃圾回收,因此最好确保不要过度使用它们。以下是一些确保这一点的方法。

避免意外的全局变量

当你为未声明的变量赋值时,JavaScript 会在默认模式下自动将其提升为全局变量。这可能是由于拼写错误造成的,并可能导致内存泄漏。另一种情况是将变量赋值给this,这在 JavaScript 中仍然是一个难以逾越的障碍。

// This will be hoisted as a global variable
function hello() {
    foo = "Message";
}

// This will also become a global variable as global functions have
// global `this` as the contextual `this` in non strict mode
function hello() {
    this.foo = "Message";
}
Enter fullscreen mode Exit fullscreen mode

'use strict';为了避免此类意外,请始终在 JS 文件顶部使用注解以严格模式编写 JavaScript 。在严格模式下,上述操作将导致错误。当您使用 ES 模块或 TypeScript 或 Babel 等转译器时,无需启用严格模式,因为它会自动启用。在较新版本的 NodeJS 中,您可以通过--use_strict在运行node命令时传递标志来全局启用严格模式。

"use strict";

// This will not be hoisted as global variable
function hello() {
    foo = "Message"; // will throw runtime error
}

// This will not become global variable as global functions
// have their own `this` in strict mode
function hello() {
    this.foo = "Message";
}
Enter fullscreen mode Exit fullscreen mode

使用箭头函数时,还需要注意不要意外创建全局变量,不幸的是,严格模式对此无能为力。您可以使用no-invalid-thisESLint 中的规则来避免这种情况。如果您不使用 ESLint,请确保不要this在全局箭头函数中赋值。

// This will also become a global variable as arrow functions
// do not have a contextual `this` and instead use a lexical `this`
const hello = () => {
    this.foo = 'Message";
}
Enter fullscreen mode Exit fullscreen mode

最后,请记住不要this使用bindcall方法将全局绑定到任何函数,因为这将违背使用严格模式等的目的。

谨慎使用全局作用域

一般来说,最好尽可能避免使用全局范围,并尽可能避免使用全局变量。

  1. 尽可能不要使用全局作用域。相反,在函数内部使用局部作用域,因为局部作用域会被垃圾回收,内存也会被释放。如果由于某些限制而必须使用全局变量,请在null不再需要时将其值设置为零。
  2. 仅将全局变量用于常量、缓存和可复用的单例。不要为了避免传递值而使用全局变量。在函数和类之间共享数据时,请将值作为参数或对象属性传递。
  3. 不要在全局范围内存储大型对象。如果必须存储它们,请确保在不需要时将其清除。对于缓存对象,请设置一个处理程序来定期清理它们,并且不要让它们无限增长。

有效使用堆栈内存

尽可能多地使用堆栈变量有助于提高内存效率和性能,因为堆栈访问比堆访问快得多。这也能确保我们不会意外造成内存泄漏。当然,只使用静态数据并不实际。在实际应用中,我们必须使用大量的对象和动态数据。但我们可以遵循一些技巧来更好地利用堆栈。

  1. 尽可能避免栈变量引用堆对象。另外,不要保留未使用的变量。
  2. 解构并使用对象或数组中所需的字段,而不是将整个对象/数组传递给函数、闭包、计时器和事件处理程序。这避免了在闭包中保留对对象的引用。传递的字段可能大多是原语,它们将保留在堆栈中。
function outer() {
    const obj = {
        foo: 1,
        bar: "hello",
    };

    const closure = () {
        const { foo } = obj;
        myFunc(foo);
    }
}

function myFunc(foo) {}
Enter fullscreen mode Exit fullscreen mode

有效利用堆内存

在任何实际应用程序中都不可能避免使用堆内存,但我们可以通过遵循以下一些技巧来提高它们的效率:

  1. 尽可能复制对象,而不是传递引用。仅当对象很大且复制操作开销较大时才传递引用。
  2. 尽量避免对象突变。相反,使用对象扩展或Object.assign复制来代替。
  3. 避免创建对同一对象的多个引用。相反,应该创建该对象的副本。
  4. 使用短暂变量。
  5. 避免创建巨大的对象树。如果无法避免,请尽量使其在局部作用域内短暂存在。

正确使用闭包、计时器和事件处理程序

正如我们之前所见,闭包、计时器和事件处理程序是其他可能发生内存泄漏的地方。让我们从闭包开始,因为它们在 JavaScript 代码中最为常见。请看下面来自 Meteor 团队的代码。这会导致内存泄漏,因为longStr变量永远不会被回收,并且内存占用不断增加。详细信息请参阅这篇博文

var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        if (originalThing) console.log("hi");
    };
    theThing = {
        longStr: new Array(1000000).join("*"),
        someMethod: function () {
            console.log(someMessage);
        },
    };
};
setInterval(replaceThing, 1000);
Enter fullscreen mode Exit fullscreen mode

上面的代码创建了多个闭包,这些闭包持有对象引用。在这种情况下,可以通过在函数originalThing末尾进行 nullification 来修复内存泄漏replaceThing。创建对象的副本并遵循前面提到的不可变方法也可以避免这种情况。

使用计时器时,请务必记住传递对象副本并避免修改。此外,完成后请使用clearTimeoutclearInterval方法清除计时器。

事件监听器和观察器也一样。一旦任务完成,就清除它们。不要让事件监听器一直运行,尤其是当它们需要保留父作用域中的任何对象引用时。

结论

由于 JS 引擎的演进和语言的改进,JavaScript 中的内存泄漏问题不像以前那么严重了,但如果我们不小心,它们仍然会发生,并会导致性能问题,甚至应用程序/操作系统崩溃。确保我们的代码不会导致 NodeJS 应用程序中的内存泄漏的第一步是了解 V8 引擎如何处理内存。下一步是了解导致内存泄漏的原因。一旦我们理解了这一点,我们就可以尝试完全避免创建这些场景。当我们确实遇到内存泄漏/性能问题时,我们就会知道要寻找什么。说到 NodeJS,一些工具也可以提供帮助。例如,Node-MemwatchNode-Inspector非常适合调试内存问题。

参考

PS:如果您喜欢这篇文章,请订阅我们的新 JavaScript Sorcery 列表,以便每月深入了解更多神奇的 JavaScript 技巧和窍门。

PPS 如果您喜欢 Node 的一体化 APM 或者您已经熟悉 AppSignal,请查看 Node.js 的 AppSignal 的第一个版本

我们的客座作者Deepu K Sasidharan是JHipster平台的联合负责人。他是一位通晓多语言的开发者和云原生倡导者,目前在 Adyen 担任开发倡导者。他还是一位出版作家、会议演讲者和博主。

文章来源:https://dev.to/appsignal/avoiding-memory-leaks-in-nodejs-best-practices-for-performance-ped
PREV
Ruby 中的闭包:Blocks、Procs 和 Lambdas
NEXT
异步处理和消息队列简介