javascript - Node.js 中的内存泄漏 - 如何分析分配树/根?
问题描述
查找内存泄漏是一项非常困难的任务,尤其是涉及到使用许多第三方库的现代 JS 代码时。例如,我目前面临汇总中的内存泄漏,涉及 babel 和自定义 babel 插件。我正在探索几种常见的策略来追捕它们:
- 了解您的运行时及其内存释放方案,并遵循有关该方案的最佳实践。
- 本文声称所有现代 JS 运行时实现都使用标记和清除垃圾收集器。它的主要优势之一是它可以正确处理循环引用。(这篇文章还链接了这篇非常过时的研讨会论文。不要太在意它,因为它都是关于循环引用的,这应该不再是问题了。)
- 本文深入探讨 V8 内存管理(注意:Node 和 Chrome 均基于 V8)。
- 如果您发现内存或 GC 使用量激增超出您的预期,请分析您的堆内存配置文件以找出内存分配的位置。
- 这个 SO 答案解释了如何在 Chrome 中做到这一点,但它的链接已经过时了。这是相关 Chrome 文档的直接链接(截至 2021 年)。
- 对于Node,我发现了很多过时的信息。目前,分析堆内存配置文件的最简单方法似乎是使用实验性的--heap-prof命令行参数(例如
node --heap-prof node_modules/rollup/dist/bin/rollup -c
,分析汇总构建)。Memory
然后通过->在 Chrome 开发工具中打开它Load
。 - 分析后,我们可以了解大部分内存分配的位置/方式;但一个关键问题尚未得到解答:
- 既然你知道罪魁祸首(记忆猪)是谁,你怎么能找出他们为什么/在哪里仍然挥之不去?而且,更重要的是:内存占用对象的 GC 根(堆栈指针)是什么?
最后一个问题也是我这里的问题:我们如何分析 Node(或一般 V8 中)中的对象分配树?如何找出我在步骤 (2) 中识别的对象在哪里乱跑?
通常,正是这个问题的答案告诉我们在哪里更改代码以阻止泄漏。(当然,如果您的问题是内存流失,而不是内存泄漏,那么这个问题可能并不那么重要。)
在我的示例中,我知道内存被 Babel AST 节点和路径对象占用,但我不知道它们为什么会徘徊,即我不知道它们存储在哪里。如果你只是单独运行 Babel,你可以验证它不是 Babel 泄漏内存。我目前正在尝试各种技巧来找出它们的存储位置,但仍然没有运气。
遗憾的是,到目前为止,我还没有找到任何工具来帮助解决问题(3)。甚至相关的深入文章(如this和它的 slidedeck here)手动绘制堆分配步骤。感觉没有这样的工具,还是我错了?如果没有工具,也许在某个地方有关于这个的讨论?
解决方案
请注意,虽然您不必在 JS 中显式地释放内存,但仍然会出现内存泄漏。同时,Node 内存分析实用程序(几乎是犯罪)的文档不足。让我们了解如何使用它们。
TLDR:跳到下面的动手部分,标题为“查找内存泄漏(带有示例)”。
JS 中的内存泄漏
由于 JS 有GC,内存泄漏只有几个可能的原因:
您挂在(“保留”)不再使用的大对象上,通常在文件或全局范围内的变量中。这要么是偶然的,要么是简单(不确定)缓存方案的一部分:
let a; function f() { a = someLargeObject; }
有时对象会在保留的闭包中徘徊。例如:
let cb; function f() { const a = someLargeObject; // `a` is retained as long as `cb` cb = function g() { eval('console.log(a)'); }; }
您可以通过从不存储或手动清除这些变量来轻松修复此类内存泄漏。主要的困难是找到这些挥之不去的物体。
使用 Chrome 开发工具分析节点应用程序
首先,Node.js 和 Chrome 都使用相同的 JS 引擎:v8。因此,Chrome 开发工具团队添加节点调试和分析支持是可行的。虽然还有其他可用的工具,但 Chrome 开发工具 (CDT) 可能更成熟(并且可能有更好的资金),这就是为什么我们将(目前)专注于如何使用 Chrome 开发工具进行节点内存分析和调试。
使用 CDT 分析节点内存的主要方法有两种:
- 运行您的应用程序
--heap-prof
以生成堆配置文件日志文件。然后加载并分析CDT中的日志。 --inspect
使用/--inspect-brk
标志运行您的应用程序,以便在 CDT 中调试您的 Node 应用程序。然后根据自己的喜好使用 CDT 的Memory
选项卡(此处的文档)。
方法一:heap-prof
运行您的应用程序--heap-prof
以生成堆配置文件日志文件。然后加载并分析CDT中的日志。
脚步
heap-prof
在启用的情况下运行您的应用程序。例如:node --heap-prof app.js
- 您可以使用其他
heap-prof
相关的命令行标志进一步自定义它。
- 您可以使用其他
- 查看工作目录(通常是运行应用程序的文件夹)。有一个新文件,默认情况下名为
Heap*.heapprofile
. - 在 Chrome 中打开一个新选项卡 → 打开 CDT → 转到内存选项卡
- 在底部,按
Load
→ 选择Heap*.heapprofile
- 完毕。您现在可以看到在录制结束时仍然存在的内存被分配到了哪里。
方法 1 的注意事项
首先,此步骤允许您验证内存泄漏,并找出可能导致它的分配或对象类型。
让我们看看CDT的内存分析工具。它具有三种模式:
可悲的是,记录的日志--heap-prof
仅包含模式1的数据。但是,这种模式不足以回答OP的第三个问题:如何找出分配的对象仍然徘徊的原因/位置(即:未使用后的“保留”不再)?
如选项卡中所述:回答该问题需要第二种模式。
我不知道是否有隐藏的方法可以更改 Node 的配置文件模式,但我没有找到它。我尝试了一些东西,包括从这个未记录的Node.js CLI 标志列表中添加。
方法二:inspect
/inspect-brk
--inspect
使用/--inspect-brk
标志运行您的应用程序,以便在 CDT 中调试您的 Node 应用程序。然后根据自己的喜好使用 CDT 的Memory
选项卡(此处的文档)。
脚步
- 在调试模式下运行应用程序,并在开始时停止执行:
node --inspect-brk app.js
chrome://inspect
在 Chrome 中打开。- 几秒钟后,您的应用程序应显示在列表中。选择它。
- CDT 已启动,您会看到执行在应用程序的入口点停止。
- 转到内存选项卡,选择第二种模式,然后按“记录”按钮
- 继续执行,直到记录内存泄漏。为此,要么在某处设置断点,要么,如果泄漏一直持续到最后,让应用程序自然退出。
- 返回“内存”选项卡并再次按“录制”按钮以停止录制。
- 您现在可以分析日志(见下文)。
方法 2 的注意事项
- 因为您现在正在调试模式下运行整个应用程序,所以一切都慢了很多。
- 堆模式 2 通常需要更多内存。如果内存超过你的 Node 默认内存限制(大约 2gb),它就会崩溃。监控您的内存使用情况,并可能使用类似
--max-old-space-size=4096
(或更大的数字)将默认值加倍。或者,如果可能的话,更好的是,简化您的测试用例以使用更少的内存并加快分析速度。 - “记录分配堆栈”选项显示分配任何对象时的调用堆栈。这类似于 Profile 模式 1 的功能。不需要查找内存泄漏。到目前为止我还不需要它,但是如果您需要将延迟对象映射到它们的分配,这应该会有所帮助。
查找内存泄漏(带有示例)
遵循方法 2 的步骤后,您现在正在查看查找泄漏所需的所有信息。
让我们看一些基本的例子:
示例 1
代码
下面的代码举例说明了一个简单的内存泄漏:文件范围内a
永久存储数据。
let a;
function test1() {
const b = [];
addPressure(N, b);
a = b;
gc(); // --expose-gc
}
test1();
debugger;
笔记:
- 寻找“挥之不去”的对象是我们的目标;属于“不可收集”的物品;已保留的对象,即使它们不再使用。这就是为什么我通常会
gc
在分析时调用。通过这种方式,我们可以确保我们摆脱所有可收集的引用,并明确关注“延迟”对象。- 您需要通话
expose-gc
标志;gc()
例如:node --inspect-brk --expose-gc app.js
- 您需要通话
内存视图
一旦断点命中,我就会停止记录,我得到了这个:
- 该
Constructor
视图列出了所有延迟对象,按构造函数/类型分组。- 确保您按
Shallow Size
或排序Retained Size
(两者都在这里解释)
- 确保您按
- 我们发现这
string
占用了大部分内存。让我们打开它。- 在 each 下方
Constructor
,您可以找到所有单个对象的列表。第一个(最大的)对象通常是罪魁祸首。选择第一个。
- 在 each 下方
- 该
Retainers
视图现在向您显示该对象仍在保留的位置。- 在这里,您想找到长期保留它的功能(使其“流连忘返”)。
有关Retainers
视图的文档并不完整。这就是我尝试导航它的方式,直到它吐出我正在寻找的代码行:
- 选择一个对象。
- (同样,通过这个列表通常最容易处理,按大小排序。)
- 在对象的树视图条目内:打开嵌套的树视图条目。
- 查找与代码行相关的任何内容(显示在第一列的右侧)。
- 标有“上下文”的条目可能比其他条目更有用。
我的发现显示在此屏幕截图中:
我们看到三个函数在这个对象的徘徊中发挥作用:
- 调用的函数
gc
- 我不确定这是为什么。可能与 GC 内部有关。可能是因为gc
会缓存对某些(如果不是全部)延迟对象的引用。 - 该
addPressure
函数分配了对象。这也是保留它的引用的来源。 - 该
test1
函数是我们将对象分配给 file-scoped 的地方a
。- 这才是真正的泄露!我们可以通过不将其分配给 来修复它
a
,或者确保a
在不再使用它之后清除它。
- 这才是真正的泄露!我们可以通过不将其分配给 来修复它
结论
我希望,这可以帮助您开始寻找和消除内存泄漏的激动人心的旅程。请随时在下面询问更多信息。
推荐阅读
- r - 如何从 R 中的非 gmail 帐户发送电子邮件
- reactjs - Redux 存储更新后 React 组件未收到 props
- php - 未显示 MongoDB 数据
- ios - 裁剪后在滚动视图上显示更新的图像
- python - 如何从 numpy 二维数组中添加或删除特定元素?
- android - 如何将 ColorDrawable 转换为位图?
- sql - Google 大查询 - Firebase 分析 - 屏幕视图的封闭漏斗(参数)
- java - 我们可以在 JDBCIO.write 函数中使用 Apache Beam 管道在单个 CloudSQL 连接中执行多个插入查询吗?
- javascript - React.js / CRA,如何从另一个文件调用或导入 javaScript 函数?
- ios - Swift:为掷骰子游戏实现游戏循环的更好方法是什么(不使用等待延迟)