【问题标题】:Are there memory leaks with javascript call a function in a callback recursively?javascript 在回调中递归调用函数是否存在内存泄漏?
【发布时间】:2015-07-27 15:18:13
【问题描述】:

假设,例如,您正在编写一个程序,该程序等待队列中的一条消息,处理它,然后等待下一条消息,这样一直持续下去。在像 C 或 Java 这样的语言中,它看起来像这样:

void processMessage() {
    while (true) {
        // waitForMessage blocks until the next message is received
       msg = waitForMessage(); 
      // handle msg here
    }
}

在Javascript中(我使用的是node.js,顺便说一句),因为使用了回调,它通常看起来像这样:

function processMessage() {
    waitForMessage(function(msg) {
        // handle msg or error here
        processMessage();
    }); 
}

我担心的是你基本上有一个递归调用原始函数的回调链,这样做的开销可能会慢慢消耗内存。我猜这实际上不是问题,因为javascript回调可能独立存在于它们自己的堆栈中并且没有被推送到原始函数的堆栈中?有人向我解释了 javascript 回调和范围,并向我保证 javascript 代码在运行任意长的时间同时接收任意大量的消息时不会耗尽内存

【问题讨论】:

  • 这并不是真正的递归,因为当触发异步事件发生时,每个回调都会在新堆栈上调用。换句话说,第一次调用processMessage() 将在下一次响应事件发生时完成。
  • 如果您设置了应该运行多次的情况,您将能够在 chrome 检查器中看到这一点。我的猜测是,只要您完成每个回调的过程,就不应该有问题

标签: javascript node.js recursion memory-leaks callback


【解决方案1】:

不,递归函数调用不会导致 Javascript 中的内存泄漏。

函数调用使用的唯一内存是一点堆栈空间(因此解释器知道函数返回时去哪里)以及函数的作用域对象使用的任何内存(例如局部变量)。当函数调用返回时,该堆栈内存完成返回给系统。它不会泄漏。

在 JavaScript 中,使用异步回调,初始化函数已经返回,因此在调用异步回调之前很久就已经清除了堆栈,因此没有建立堆栈。

在回调完成之前,内存中会有一个函数作用域对象,这很重要,并且是允许内联回调访问在其父作用域中声明的变量所必需的。一旦回调完成(不再可访问),该范围对象将被垃圾收集。除非你正在做一些不寻常的事情,例如在那个临时范围内分配巨大的字符串或缓冲区,否则该范围的内存使用应该不是问题。

关于从一个初始函数调用中获取许多消息,然后重复调用同一个回调,请记住,父函数只执行一次,并且无论调用多少次回调都只分配一个范围对象,所以每次调用回调时都不会增加内存。回调本身每次调用时都会获得一个新的函数作用域,但由于回调本身不会触发任何异步调用,因此该作用域对象将是临时的,并且一旦回调完成就可以进行垃圾回收工作并返回。

如果您将异步操作相互链接/嵌入,则在异步操作期间将保留其他范围对象,但这就是 Javascript 的工作方式,并且提供了访问父范围的功能。在实践中,它通常没有被证明是内存问题。范围对象本身是相对紧凑的对象(几乎每个函数调用都创建一个),所以正如我上面所说,只要你不将巨大的缓冲区或巨大的字符串/数组放入持久范围,内存使用通常是不相关的.


还请记住,当您从异步回调中再次调用 processMessage() 时,这不是您通常会想到的那种递归,因为之前对 processMessage() 的函数调用已经返回并且堆栈已经返回在异步事件触发回调之前完全展开。因此,在这种情况下没有堆栈堆积。这是因为 Javascript 中的异步操作都是通过事件队列运行的。当异步操作准备好触发一个动作时,它会将一个事件放入 Javascript 事件队列中。该事件仅在 JS 操作的当前线程完成并完全展开时才会处理。只有这样 JS 解释器才会在事件队列中查看是否还有其他事情要做。因此,堆栈总是在触发下一个异步操作之前完全展开。

有关其工作原理的更多信息以及有关 JS 事件队列的一些参考文章(在 node.js 中的工作方式与在浏览器中的工作方式相同),请参阅这篇文章:

How does JavaScript handle AJAX responses in the background?

这是 Joyent 将 node.js 称为 “事件驱动的非阻塞 I/O 模型” 的原因之一,就在 node.js home page .

【讨论】:

  • 您先说没有内存泄漏,然后说“函数调用使用的唯一内存是一点堆栈空间......”。少量内存并不能减少内存泄漏。我很确定,如果你无限递归地调用一个函数,你最终会出现内存溢出。
  • 函数 a(){var b=[100]; for(var i=0; i
  • @icenac - 堆栈空间不是内存泄漏,它是内存使用量。它只是在函数返回之前使用的内存。无限递归地调用同步函数将很快遇到堆栈溢出。这是与内存泄漏完全不同的问题。需要避免的事情,但是与内存泄漏不同的一类问题。
  • 在我的示例中,数组肯定是在堆上分配的。并且由于没有退出范围,堆使用量会随着时间的推移而增加。由于多种原因,它没有在堆栈上分配。 1)这是一个脚本,不是可执行文件,所以只有浏览器进程有真正的堆栈,而不是其中运行的脚本。 2)如果我分配一个 100M 字节的数组,无论如何它都不能分配到堆栈上,因为堆栈在大多数现代操作系统中受到更多限制。
  • @icenac - 我不明白你的意思。您认为我对此处提出的问题的回答中哪一部分不正确或需要澄清。如果您的示例正在消耗越来越多的内存,那么这可能只是因为您没有允许足够的时间进行垃圾收集,或者您没有查看实际使用的内存与进程内存。您的局部变量 b 没有持久引用,因此它有资格进行垃圾收集,并且应该在 GC 运行时进行 GC。并且,范围已退出。 setTimeout() 是非阻塞的。
【解决方案2】:

here is great description abut nextTick()

function processMessage() {
    waitForMessage(function(msg) {
        // handle msg or error here
        process.nextTick(processMessage;
    }); 
}

【讨论】:

    【解决方案3】:

    函数不在堆栈上分配。除非出于某种原因保留引用,否则回调函数将在使用后被垃圾回收。你的代码应该没问题!

    【讨论】:

    • 函数调用被放入堆栈,作为跟踪函数返回时执行的位置的一种方式。函数作用域对象没有放入堆栈,因为它们的生命周期可能与函数本身的执行不同,因此它们像 Javascript 中的其他对象一样被垃圾收集。
    猜你喜欢
    • 2012-08-25
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    • 2015-07-14
    • 2021-10-06
    • 2021-07-11
    • 2013-06-15
    • 1970-01-01
    相关资源
    最近更新 更多