【问题标题】:Node.js - Maximum call stack size exceededNode.js - 超出最大调用堆栈大小
【发布时间】:2014-01-23 01:28:06
【问题描述】:

当我运行我的代码时,Node.js 抛出一个由太多递归调用引起的 "RangeError: Maximum call stack size exceeded" 异常。我尝试将 Node.js 堆栈大小增加 sudo node --stack-size=16000 app,但 Node.js 崩溃且没有任何错误消息。当我在没有 sudo 的情况下再次运行它时,Node.js 会打印出'Segmentation fault: 11'。有没有可能在不删除我的递归调用的情况下解决这个问题?

【问题讨论】:

  • 为什么首先需要这么深的递归?
  • 拜托,你能发布一些代码吗? Segmentation fault: 11 通常表示节点中的错误。
  • @Dan Abramov:为什么要深度递归?如果您希望遍历数组或列表并对每个数组或列表执行异步操作(例如某些数据库操作),这可能会成为问题。如果您使用来自异步操作的回调移动到下一个项目,那么列表中的每个项目将至少有一个额外的递归级别。下面由 heinob 提供的反模式阻止堆栈溢出。
  • @PhilipCallender 我没有意识到你在做异步的东西,谢谢你的澄清!
  • @DanAbramov 也不必很深才能崩溃。 V8 没有机会清除堆栈上分配的内容。早已停止执行的较早调用的函数可能在堆栈上创建了不再引用但仍保存在内存中的变量。如果您以同步方式执行任何密集的耗时操作并在堆栈上分配变量,那么您仍然会因相同的错误而崩溃。我的同步 JSON 解析器在调用堆栈深度为 9 时崩溃。kikobeats.com/synchronously-asynchronous

标签: node.js recursion stack-overflow callstack


【解决方案1】:

你可以使用循环for。

var items = {1, 2, 3}
for(var i = 0; i < items.length; i++) {
  if(i == items.length - 1) {
    res.ok(i);
  }
}

【讨论】:

  • var items = {1, 2, 3} 不是有效的 JS 语法。这与问题有什么关系?
  • 我认为他的意思是[1,2,3]
【解决方案2】:

上一篇:

对我来说,具有 Max 调用堆栈的程序不是因为我的代码。它最终成为一个不同的问题,导致应用程序流出现拥塞。因此,因为我试图在没有任何配置机会的情况下向 mongoDB 添加太多项目,所以调用堆栈问题突然出现,我花了几天时间才弄清楚发生了什么......也就是说:


跟进@Jeff Lowery 的回答:我非常喜欢这个答案,它至少将我正在做的事情加快了 10 倍。

我是编程新手,但我试图将答案模块化。 另外,不喜欢抛出的错误,所以我将它包装在一个 改用while循环。如果我做的任何事情都不正确, 请随时纠正我。

module.exports = function(object) {
    const { max = 1000000000n, fn } = object;
    let counter = 0;
    let running = true;
    Error.stackTraceLimit = 100;
    const A = (fn) => {
        fn();
        flipper = B;
    };
    const B = (fn) => {
        fn();
        flipper = A;
    };
    let flipper = B;
    const then = process.hrtime.bigint();
    do {
        counter++;
        if (counter > max) {
            const now = process.hrtime.bigint();
            const nanos = now - then;
            console.log({ 'runtime(sec)': Number(nanos) / 1000000000.0 });
            running = false;
        }
        flipper(fn);
        continue;
    } while (running);
};

查看此要点以查看我的文件以及如何调用循环。 https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c

【讨论】:

    【解决方案3】:

    我想到了另一种使用函数引用来限制调用堆栈大小而不使用setTimeout()的方法(Node.js, v10.16.0)

    testLoop.js

    let counter = 0;
    const max = 1000000000n  // 'n' signifies BigInteger
    Error.stackTraceLimit = 100;
    
    const A = () => {
      fp = B;
    }
    
    const B = () => {
      fp = A;
    }
    
    let fp = B;
    
    const then = process.hrtime.bigint();
    
    for(;;) {
      counter++;
      if (counter > max) {
        const now = process.hrtime.bigint();
        const nanos = now - then;
    
        console.log({ "runtime(sec)": Number(nanos) / (1000000000.0) })
        throw Error('exit')
      }
      fp()
      continue;
    }
    

    输出:

    $ node testLoop.js
    { 'runtime(sec)': 18.947094799 }
    C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25
        throw Error('exit')
        ^
    
    Error: exit
        at Object.<anonymous> (C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25:11)
        at Module._compile (internal/modules/cjs/loader.js:776:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
        at Module.load (internal/modules/cjs/loader.js:653:32)
        at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
        at Function.Module._load (internal/modules/cjs/loader.js:585:3)
        at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
        at startup (internal/bootstrap/node.js:283:19)
        at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
    

    【讨论】:

      【解决方案4】:

      我遇到了类似的问题。 我在连续使用多个 Array.map() 时遇到问题(一次大约 8 个地图) 并收到 maximum_call_stack_exceeded 错误。 我通过将地图更改为“for”循环解决了这个问题

      因此,如果您使用了大量的 map 调用,将它们更改为 for 循环可能会解决问题

      编辑

      为了清楚起见和可能不需要但很好知道的信息,使用 .map() 会导致数组被准备好(解析 getters 等)并缓存回调,并且还在内部保留数组的索引(因此回调提供了正确的索引/值)。这与每个嵌套调用一起堆叠,并且在不嵌套时也建议小心,因为可以在第一个数组被垃圾收集之前调用下一个 .map()(如果有的话)。

      举个例子:

      var cb = *some callback function*
      var arr1 , arr2 , arr3 = [*some large data set]
      arr1.map(v => {
          *do something
      })
      cb(arr1)
      arr2.map(v => {
          *do something // even though v is overwritten, and the first array
                        // has been passed through, it is still in memory
                        // because of the cached calls to the callback function
      }) 
      

      如果我们将其更改为:

      for(var|let|const v in|of arr1) {
          *do something
      }
      cb(arr1)
      for(var|let|const v in|of arr2) {
          *do something  // Here there is not callback function to 
                         // store a reference for, and the array has 
                         // already been passed of (gone out of scope)
                         // so the garbage collector has an opportunity
                         // to remove the array if it runs low on memory
      }
      

      我希望这有点道理(我没有最好的语言表达方式)并帮助一些人防止我经历过的挠头

      如果有人有兴趣,这里也是一个比较map和for循环的性能测试(不是我的工作)。

      https://github.com/dg92/Performance-Analysis-JS

      for 循环通常比 map 好,但不是 reduce、filter 或 find

      【讨论】:

      • 几个月前,当我阅读您的回复时,我不知道您的回答中有多少金子。我最近为自己发现了同样的事情,它真的让我想忘掉我所拥有的一切,有时很难以迭代器的形式思考。希望这会有所帮助:: 我写了一个额外的例子,其中包括作为循环的一部分的承诺,并展示了如何在继续之前等待响应。例如:gist.github.com/gngenius02/…
      • 我喜欢你在那儿所做的事情(希望你不介意我为我的工具箱截取那个片段)。我主要使用同步代码,这就是为什么我通常更喜欢循环。但这也是你得到的一块宝石,很可能会在我工作的下一个服务器上找到它
      【解决方案5】:

      请检查您正在导入的函数和您在同一文件中声明的函数名称是否相同。

      我会给你一个这个错误的例子。在 express JS(使用 ES6)中,考虑以下场景:

      import {getAllCall} from '../../services/calls';
      
      let getAllCall = () => {
         return getAllCall().then(res => {
            //do something here
         })
      }
      module.exports = {
      getAllCall
      }
      

      上述情况会导致臭名昭著的 RangeError: Maximum call stack size exceeded 错误,因为该函数不断调用自身多次以致用完最大调用堆栈。

      大多数时候错误是在代码中(如上面的那个)。其他解决方法是手动增加调用堆栈。好吧,这适用于某些极端情况,但不建议这样做。

      希望我的回答对你有所帮助。

      【讨论】:

        【解决方案6】:

        你应该把你的递归函数调用包装成一个

        • setTimeout,
        • setImmediate
        • process.nextTick

        函数让 node.js 有机会清除堆栈。如果您不这样做并且有许多没有任何真正异步函数调用的循环,或者如果您不等待回调,您的RangeError: Maximum call stack size exceeded不可避免

        有很多关于“潜在异步循环”的文章。 Here is one

        现在还有一些示例代码:

        // ANTI-PATTERN
        // THIS WILL CRASH
        
        var condition = false, // potential means "maybe never"
            max = 1000000;
        
        function potAsyncLoop( i, resume ) {
            if( i < max ) {
                if( condition ) { 
                    someAsyncFunc( function( err, result ) { 
                        potAsyncLoop( i+1, callback );
                    });
                } else {
                    // this will crash after some rounds with
                    // "stack exceed", because control is never given back
                    // to the browser 
                    // -> no GC and browser "dead" ... "VERY BAD"
                    potAsyncLoop( i+1, resume ); 
                }
            } else {
                resume();
            }
        }
        potAsyncLoop( 0, function() {
            // code after the loop
            ...
        });
        

        这是对的:

        var condition = false, // potential means "maybe never"
            max = 1000000;
        
        function potAsyncLoop( i, resume ) {
            if( i < max ) {
                if( condition ) { 
                    someAsyncFunc( function( err, result ) { 
                        potAsyncLoop( i+1, callback );
                    });
                } else {
                    // Now the browser gets the chance to clear the stack
                    // after every round by getting the control back.
                    // Afterwards the loop continues
                    setTimeout( function() {
                        potAsyncLoop( i+1, resume ); 
                    }, 0 );
                }
            } else {
                resume();
            }
        }
        potAsyncLoop( 0, function() {
            // code after the loop
            ...
        });
        

        现在您的循环可能会变得太慢,因为我们每轮都会浪费一点时间(一次浏览器往返)。但是您不必在每一轮中都致电setTimeout。正常情况下是可以的。每 1000 次执行一次。但这可能会因您的堆栈大小而异:

        var condition = false, // potential means "maybe never"
            max = 1000000;
        
        function potAsyncLoop( i, resume ) {
            if( i < max ) {
                if( condition ) { 
                    someAsyncFunc( function( err, result ) { 
                        potAsyncLoop( i+1, callback );
                    });
                } else {
                    if( i % 1000 === 0 ) {
                        setTimeout( function() {
                            potAsyncLoop( i+1, resume ); 
                        }, 0 );
                    } else {
                        potAsyncLoop( i+1, resume ); 
                    }
                }
            } else {
                resume();
            }
        }
        potAsyncLoop( 0, function() {
            // code after the loop
            ...
        });
        

        【讨论】:

        • 您的回答有好有坏。我真的很喜欢你提到 setTimeout() 等。但是没有必要使用 setTimeout(fn, 1),因为 setTimeout(fn, 0) 非常好(所以我们不需要每 % 1000 hack 的 setTimeout(fn, 1))。它允许 JavaScript VM 清除堆栈,并立即恢复执行。在 node.js 中 process.nextTick() 稍微好一点,因为它允许 node.js 在让你的回调恢复之前做一些其他的事情(I/O IIRC)。
        • 我会说在这些情况下最好使用 setImmediate 而不是 setTimeout。
        • @joonas.fi:我的 %1000 的“hack”是必要的。在 每个 循环上执行 setImmediate/setTimeout(即使为 0)会非常慢。
        • 关心用英文翻译更新您的代码内德语 cmets...?:) 我明白,但其他人可能没那么幸运。
        【解决方案7】:

        关于增加最大堆栈大小,在 32 位和 64 位机器上,V8 的内存分配默认值分别为 700 MB 和 1400 MB。在较新版本的 V8 中,V8 不再设置 64 位系统的内存限制,理论上表示没有限制。但是,运行 Node 的 OS(操作系统)总是会限制 V8 可以占用的内存量,因此无法概括说明任何给定进程的真正限制。

        尽管 V8 提供了 --max_old_space_size 选项,它允许控制进程可用的内存量,接受以 MB 为单位的值。如果您需要增加内存分配,只需在生成 Node 进程时将此选项传递给所需的值即可。

        减少给定节点实例的可用内存分配通常是一种极好的策略,尤其是在运行多个实例时。与堆栈限制一样,请考虑是否将大量内存需求更好地委派给专用存储层,例如内存数据库或类似的。

        【讨论】:

          【解决方案8】:

          如果您不想实现自己的包装器,可以使用队列系统,例如async.queue, queue.

          【讨论】:

            【解决方案9】:

            我找到了一个肮脏的解决方案:

            /bin/bash -c "ulimit -s 65500; exec /usr/local/bin/node --stack-size=65500 /path/to/app.js"
            

            它只是增加了调用堆栈限制。我认为这不适合生产代码,但我需要它用于只运行一次的脚本。

            【讨论】:

            • 很酷的把戏,虽然我个人建议使用正确的做法来避免错误并创建更全面的解决方案。
            • 对我来说,这是一个畅通无阻的解决方案。我有一个场景,我正在运行数据库的第三方升级脚本并且遇到范围错误。我不打算重写第三方包但需要升级数据库→这修复了它。
            【解决方案10】:

            在某些语言中,这可以通过尾调用优化来解决,其中递归调用在后台转换为循环,因此不存在达到最大堆栈大小错误。

            但在 javascript 中,当前引擎不支持这一点,预计新版本的语言 Ecmascript 6

            Node.js 有一些标志来启用 ES6 功能,但尾调用尚不可用。

            因此,您可以重构代码以实现称为trampolining 的技术,或重构为transform recursion into a loop

            【讨论】:

            • 谢谢。我的递归调用没有返回值,那么有什么方法可以调用函数而不等待结果呢?
            • 它的功能是否会改变一些数据,比如数组,它的功能是什么,输入/输出是什么?
            猜你喜欢
            • 2015-02-07
            • 1970-01-01
            • 1970-01-01
            • 2012-05-27
            • 2016-12-04
            • 2014-12-03
            • 1970-01-01
            • 2015-10-24
            • 2015-12-29
            相关资源
            最近更新 更多