2014-01-05 57 views
60

當我運行我的代碼時,Node.js拋出"RangeError: Maximum call stack size exceeded"異常導致的遞歸調用過多。我試圖通過sudo node --stack-size=16000 app增加Node.js堆棧大小,但Node.js崩潰時沒有任何錯誤消息。當我沒有sudo再次運行這個時,Node.js打印'Segmentation fault: 11'。有沒有可能解決這個問題,而不刪除遞歸調用?Node.js - 超出最大調用堆棧大小

感謝

+2

爲什麼你首先需要如此深的遞歸? –

+0

請問,你可以發表一些代碼嗎? 'Segmentation fault:11'通常意味着節點中存在一個錯誤。 – vkurchatkin

+1

@丹Abramov:爲什麼深遞歸?如果您希望遍歷數組或列表並對每個數據庫執行異步操作(例如某些數據庫操作),則這可能會成爲問題。如果您使用異步操作中的回調移動到下一個項目,那麼對於列表中的每個項目,至少會有一個額外的遞歸級別。 heinob提供的反模式可以阻止煙囪吹出。 –

回答

77

你應該換你的遞歸函數調用到

  • setTimeout
  • setImmediate
  • process.nextTick

功能給node.js的清除棧的機會。如果你不這樣做,並且有很多循環沒有任何真實異步函數調用,或者如果你不等待回調,你的RangeError: Maximum call stack size exceeded不可避免的

有很多關於「潛在的異步循環」的文章。 Here is one

現在一些示例代碼:

這是正確的:

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

答案中有一些好的和壞的點。我真的很喜歡你提到的setTimeout()等人。但是沒有必要使用setTimeout(fn,1),因爲setTimeout(fn,0)非常好(所以我們不需要每過%1000 hack就設置setTimeout(fn,1))。它允許JavaScript VM清除堆棧,並立即恢復執行。在node.js中,process.nextTick()稍微好一些,因爲它允許node.js在讓你的回調繼續之前做一些其他的事情(I/O IIRC)。 –

+1

你是對的。 0更好。修復。 – heinob

+1

我會說在這些情況下最好使用setImmediate而不是setTimeout。 – BaNz

5

在某些語言中,這可以用尾巴調用優化,其中遞歸調用引擎蓋下轉化爲一個循環存在所以沒有最大堆棧大小達到了錯誤來解決。

但在JavaScript中,當前引擎不支持此功能,因此可預見新版本的語言Ecmascript 6

Node.js有一些標誌來啓用ES6功能,但尾呼叫尚不可用。

因此,您可以重構代碼以實現名爲trampolining的技術,或者重構爲transform recursion into a loop

+0

謝謝。我的遞歸調用不返回值,所以有什麼辦法可以調用函數,而不是等待結果? – user1518183

+0

並且它改變了一些數據的功能,比如數組,它做了什麼功能,輸入/輸出是什麼? –

19

我發現了一個骯髒的解決方案:

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

它只是增加調用堆棧限制。我認爲這不適合生產代碼,但我只需要它運行一次的腳本。

+13

祝你好運! – heinob

1

如果您不希望實現自己的包裝,你可以使用一個排隊系統,例如:但是,這可能取決於你的籌碼大小不同async.queuequeue

0

關於增加最大堆棧大小,在32位和64位機器上,V8的內存分配默認值分別爲700 MB和1400 MB。在較新版本的V8中,64位系統的內存限制不再由V8設置,理論上沒有限制。但是,運行Node的OS(操作系統)可以始終限制V8可以使用的內存量,因此任何給定進程的真正限制都不能一概而論。

儘管V8提供了--max_old_space_size選項,該選項允許控制進程可用的內存量,接受以MB爲單位的值。如果您需要增加內存分配,只需在產生節點進程時將此選項傳遞給期望值即可。

減少給定節點實例的可用內存分配通常是一種很好的策略,特別是在運行多個實例時。與堆棧限制一樣,考慮將大容量內存需求委派給專用存儲層(如內存數據庫或類似存儲層)是否更好。

0

請檢查您正在導入的功能和您在同一文件中聲明的功能名稱不同。

我會給你一個這個錯誤的例子。在(使用ES6)快遞JS,考慮以下情形:

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

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

上述情況會造成臭名昭著的RangeError:最大調用堆棧大小超過錯誤,因爲該功能保持自稱這麼多次,它用完最大調用堆棧。

大部分時間錯誤代碼(如上面的那樣)。其他解決方法是手動增加調用堆棧。那麼,這適用於某些極端情況,但不建議。

希望我的回答對你有幫助。

相關問題