Node.js - 超出最大调用堆栈大小

时间:2014-01-05 17:08:48

标签: node.js stack-overflow callstack

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

由于

10 个答案:

答案 0 :(得分:89)

您应该将递归函数调用包装到

  • 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。通常是o.k.每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
    ...
});

答案 1 :(得分:20)

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

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

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

答案 2 :(得分:5)

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

但是在javascript中,当前引擎不支持此功能,预计新版本的语言Ecmascript 6

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

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

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

因此,如果您使用了大量的地图调用,请将它们更改为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更好,但不能减少,过滤或查找

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

请检查您导入的功能和您在同一文件中声明的功能是否具有相同的名称。

我将举例说明此错误。在express JS(使用ES6)中,请考虑以下场景:

import {getAllCall} from '../../services/calls';

let getAllCall = () => {
   return getAllCall().then(res => {
      //do something here
   })
}
module.exports = {
getAllCall
}

上述情况将导致臭名昭着的 RangeError:超出最大调用堆栈大小错误,因为该函数一直调用自身很多次,以至于它耗尽了最大调用堆栈。

大部分时间错误都在代码中(如上所述)。其他解决方法是手动增加调用堆栈。嗯,这适用于某些极端情况,但不建议这样做。

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

答案 7 :(得分:0)

我想到了另一种使用函数引用的方法,该方法限制了调用堆栈的大小而不使用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)

答案 8 :(得分:0)

上一个:

对我来说,具有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

答案 9 :(得分:-4)

您可以使用循环。

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