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


80

当我运行代码时,Node.js会"RangeError: Maximum call stack size exceeded"引发由过多的递归调用引起的异常。我尝试将Node.js堆栈大小增加sudo node --stack-size=16000 app,但Node.js崩溃而没有任何错误消息。当我不使用sudo再次运行此命令时,Node.js将显示'Segmentation fault: 11'。是否有可能在不删除递归调用的情况下解决此问题?


3
为什么首先需要这样的深度递归?
Dan Abramov 2014年

拜托,您可以张贴一些代码吗?Segmentation fault: 11通常表示节点中有错误。
vkurchatkin 2014年

1
@Dan Abramov:为什么要进行深度递归?如果您希望遍历数组或列表并对每个数组或列表执行异步操作(例如,某些数据库操作),则可能会出现问题。如果您使用异步操作中的回调进行到下一项,那么列表中的每一项至少会有一个额外的递归级别。下面的heinob提供的反模式可防止堆栈爆裂。
菲利普·卡伦德2014年

1
@PhilipCallender我没意识到你在做异步工作,感谢您的澄清!
丹·阿布拉莫夫

@DanAbramov不必太深即可崩溃。V8没有机会清除分配在堆栈上的内容。早已停止执行的函数可能已经在堆栈上创建了不再被引用但仍保留在内存中的变量。如果您以同步方式进行任何耗时的密集操作,并在堆栈上分配变量时,仍然会因相同的错误而崩溃。我让我的同步JSON解析器在调用堆栈深度为9时 崩溃。kikobeats.com/synchronously
asynchronous

Answers:


113

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

  • setTimeout
  • setImmediate 要么
  • process.nextTick

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

有许多有关“潜在异步循环”的文章。这是一个

现在再看一些示例代码:

// 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每次都跟注。通常,每千次可以这样做。但这可能会有所不同,具体取决于您的堆栈大小:

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
    ...
});

6
您的回答有好有坏。我真的很喜欢您提到setTimeout()等。但是没有必要使用setTimeout(fn,1),因为setTimeout(fn,0)非常好(因此,我们不需要每%1000 hack的setTimeout(fn,1))。它允许JavaScript VM清除堆栈,并立即恢复执行。在node.js中,process.nextTick()稍好一点,因为它允许node.js在允许您的回调恢复之前还做一些其他事情(I / O IIRC)。
joonas.fi 2014年

2
我想说在这些情况下最好使用setImmediate而不是setTimeout。
BaNz 2014年

4
@ joonas.fi:我有%1000的“ hack”是必要的。在每个循环上执行setImmediate / setTimeout(即使为0)也要慢得多。
heinob 2014年

3
希望通过英语翻译来更新您的代码内德语注释...?:)我理解,但其他人可能并不那么幸运。
罗伯特·罗斯曼


28

我发现了一个肮脏的解决方案:

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

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


很酷的把戏,尽管我个人建议使用正确的做法来避免错误并创建更完善的解决方案。
解码器7283年

对我来说,这是一个畅通无阻的解决方案。我有一种情况,我正在运行数据库的第三方升级脚本,并且遇到范围错误。我本来不想重写第三方软件包,但需要升级数据库→这样就解决了它。
蒂姆·科克

7

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

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

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

因此,您可以重构代码以实现一种称为蹦床的技术,也可以重构以将递归转换为循环


谢谢。我的递归调用不返回值,因此有什么方法可以调用函数而不等待结果?
user1518183 2014年

它的功能是否会更改某些数据(例如数组),它的功能是什么,输入/输出是什么?
Angular University

5

我有一个类似的问题。我在连续使用多个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更好,但不减少,过滤或查找


几个月前,当我读到您的回复时,我不知道您的答案中有多少钱。最近,我为自己发现了同样的事情,它的确使我想学习所有东西,有时很难以迭代器的形式进行思考。希望对您有所帮助::我写了一个额外的示例,其中将promise作为循环的一部分,并说明了如何在继续之前等待响应。例如: gist.github.com/gngenius02/...
上cigol

我喜欢您在这里所做的事情(希望您不要介意我为我的工具箱抢到了那个)我主要使用同步代码,这就是为什么我通常更喜欢循环。但这也是您到达那里的瑰宝,很可能会找到我正在使用的下一台服务器的方式
Werlious

2

上一个:

对我来说,使用Max调用堆栈的程序并不是因为我的代码。最终成为另一个问题,导致了应用程序流程的拥塞。因此,由于我试图在mongoDB中添加太多项目而没有任何配置机会,因此出现了调用栈问题,并且花了我几天时间才弄清楚发生了什么...


跟进@Jeff Lowery的回答:我非常喜欢这个答案,它至少使我的工作速度加快了10倍。

我是编程新手,但我尝试将其答案模块化。另外,我不喜欢抛出错误,所以我将其包装在do 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



1

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

0

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

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

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


0

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

我将为您提供此错误的示例。在Express JS(使用ES6)中,请考虑以下情形:

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

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

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

在大多数情况下,错误是由代码造成的(如上述错误)。解决的其他方法是手动增加调用堆栈。好吧,这在某些极端情况下可行,但不建议这样做。

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.