无限循环中的NodeJS内存消耗

Posted

技术标签:

【中文标题】无限循环中的NodeJS内存消耗【英文标题】:NodeJS memory consumption in an infinite loop 【发布时间】:2011-12-15 14:55:36 【问题描述】:

我不知道这是 Node 还是 V8 的错误,但如果我运行以下代码,节点进程会泄漏内存。 GC 似乎永远不会启动,并且在几秒钟内它会消耗超过 1GB 的内存。这是意外行为。我错过了什么吗?

代码如下:

for(;;)  console.log(1+1); 

显然,这有点人为的情况,但我可以看到一个长期运行的进程永远不会释放内存的问题。

编辑:我尝试了 v0.5.10(不稳定)和 v0.4.12(稳定),不稳定版本的性能稍微好一点——稳定版本只是停止输出到控制台但继续消耗内存,而稳定版继续执行和消耗内存,没有暂停。

【问题讨论】:

经过一些研究,我无法准确找出为什么节点在没有 GC 的情况下会消耗越来越多的内存。一般来说,在 node.js 中使用任何类型的无限循环都是 bad 的想法。必须了解 node.js 脚本在执行输入脚本后实际上已经在运行一个事件循环(可能是无限的),因此没有理由在等待某事发生时模拟这样的循环。 (来自 nodejs.org)“节点只是在执行输入脚本后进入事件循环。当没有更多回调要执行时,节点退出事件循环。[...] 事件循环对用户隐藏。” @WTK 我知道事件循环或多或少是一个无限循环,但这种结构并不是为了保持循环存活;我在编写守护程序时偶然发现了它。此代码还有其他变体,例如使用 setInterval 以 1 毫秒的延迟重复调用回调也会导致此问题。我在 Python 和 Ruby 下尝试了类似的代码,它们的内存占用都小于 10MB。我可以想象,在负载下长时间运行的守护进程会发生类似的内存泄漏,尽管我还没有测试过。 您对触发此问题的其他可能方式是正确的,但它们基本上都是阻塞无限循环。我很确定每个经过良好测试、稳定、长时间运行的库都依赖于 node.js“本机”事件循环,而不是手动创建的某种阻塞无限循环。无论如何,我希望有人能阐明这种无限循环中发生了什么,以及为什么内存使用量不断增加。 @WTK 我同意你的看法;阻塞调用也会影响执行速度——大约是可比较的 Python 或 Ruby 代码的 1/6。我想知道在看到这个之后它是否足够稳定和安全,可以在生产中使用。 您应该将此作为错误提交。 【参考方案1】:

由于 Node.js v0.10 已经发布,调用递归回调时,setImmediate 应作为首选,而不是process.nextTick

function loginf() 
  console.log(1+1);
  setImmediate(loginf);

loginf();

在我的电脑上运行大约 15 分钟后,这段代码的内存消耗保持在较低水平 (

相反,无限运行for loop 会导致内存泄漏,process.nextTick 会抛出Maximum call stack size exceeded 错误。

也请查看此问答:setImmediate vs. nextTick

【讨论】:

【参考方案2】:

@VyacheslavEgorov 的答案似乎是正确的,但我想推迟到事件循环会解决问题。您可能想比较一下您的无限 for-loop 与此无限循环策略的比较:

function loginf() 
  console.log(1+1);
  process.nextTick(loginf);

loginf();

这个想法是使用process.nextTick(cb) 来推迟事件循环并(大概)允许 GC 做它的事情。

【讨论】:

这是内存的显着改进,而且 IIRC 也稍微快了一点。我很惊讶 setInterval 也不能这样工作。调用具有短暂延迟的函数会导致相同的内存泄漏。我想知道这些是否是 Node 中的设计问题。 @Matty:nextTick 的文档说“这不是 setTimeout(fn, 0) 的简单别名,效率更高”,所以看起来像明确的设计选择。【参考方案3】:

您将阻止 node.js 事件循环,因为您永远不会返回它。

当您向流中写入内容时,node.js 会异步执行此操作:它发送写入请求,在流的内部数据结构中排列有关已发送请求的信息,并等待通知它完成的回调。

如果您阻止事件循环,则永远不会调用回调(因为永远不会处理传入事件),并且永远不会释放流中排队的辅助数据结构。

如果您通过不断地使用 nextTick/setInterval/setTimeout 安排自己的事件来“超载”事件循环,则可能会发生同样的情况。

【讨论】:

那么基本上GC只发生在事件循环循环的时候? @Matty GC 实际上会在堆增长时发生很多次。但它不能收集活动的东西:写请求辅助结构由输出流在内部保存,直到写后回调被调用......这永远不会发生。由于事件循环被阻塞,输出流不理解他从底层请求的 IO 操作已完成,并且不再需要这些结构。

以上是关于无限循环中的NodeJS内存消耗的主要内容,如果未能解决你的问题,请参考以下文章

内存芯片实现中的无限循环问题 (Nand2Tetris)

如何从循环外部杀死处于无限循环中的 pthread?

ReactJS fetch 导致无限循环

如何从循环外部杀死无限循环中的pthread?

使用 setInterval() 的无限异步循环 [重复]

导致内存错误的无限循环错误