Maximum call stack size exceeded 棧溢出的解釋

問題

工作過程中我們時常會碰到棧溢出的問題,而這經常是由死循環引起的,見下面的代碼。

function foo() {
  foo()
}
foo()


VM398:1 Uncaught RangeError: Maximum call stack size exceeded
    at foo (<anonymous>:1:13)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)
    at foo (<anonymous>:2:3)

那今日看了李兵老師的圖解 Google V8-堆和棧:函數調用時如何影響到內存佈局的,筆者纔對棧溢出有了更深的瞭解。

首先爲什麼會使用棧的結構來管理函數調用?

這是因爲在父函數中調用子函數,執行代碼的控制權是從父轉移到子,子執行完畢在將執行代碼控制權轉移給父,這個過程就符合先進後出的棧結構。

壓棧過程

先來看一段 c 代碼代碼

int add(num1,num2){
    int x = num1;
    int y = num2;
    int ret = x + y;
    return ret;
}


int main()
{
    int x = 5;
    int y = 6;
    x = 100;
    int z = add(x,y);
    return z;
}

他在執行過程中棧的變化如下圖

接下來,就要調用 add 函數了,理想狀態下,執行 add 函數的過程是下面這樣的

add 函數執行完後,需要將代碼控制權轉移給 main 主函數,這個過程叫__恢復現場__。那怎麼恢復現場?

解決的方法就是加指針,保存函數調用棧的位置。在寄存器中保存一個永遠指向當前棧頂的指針,再保持一個當前函數起始位置的指針叫棧幀指針

等到 add 函數執行完成,直接將 ebp 指針的指向寫給 esp 指針就成了,甚至都不需要逐個彈出棧頂元素。

補充下棧幀的概念,因爲在很多文章中我們會看到這個概念,每個棧幀對應着一個未運行完的函數,棧幀中保存了該函數的返回地址和局部變量。

棧是線性的結構,並且容量是固定的,所以重複嵌套執行一個函數就會導致棧的溢出。

堆用來幹啥

在棧上分配資源和銷燬資源的速度非常快,這主要歸結於棧空間是連續的,分配空間和銷燬空間只需要移動下指針就可以了。而大的對象經常需要增刪,變動空間,就不適合使用棧給它分配連續的大空間了,自然而然的就把對象這樣的複雜結構放入堆中,而棧只是存它的引用地址。

思考

function foo() {
  setTimeout(foo, 0) // 是否存在堆棧溢出錯誤?
}

答:不會,它能正常執行。因爲它在每次宏任務事件循環裏才觸發一次,而後銷燬棧幀再觸發另一次,不會爆棧。

function foo() {
  // 是否存在堆棧溢出錯誤?
  return Promise.resolve().then(foo)
}
foo()

答:也不會,它雖能執行,但是無限卡死頁面了。和上面類似,它在每次微任務裏觸發一次,而後銷燬棧幀觸發另一次,也就是添加了一個新的微任務。而在事件循環機制裏,一個宏任務需要等它的所有微任務觸發完畢纔會進入下一個事件循環邏輯,故而 JS 主線程的事件循環在這裏被卡着了,頁面就被卡了。

筆者特意在 node 12 裏運行,也發現控制檯直接定住,過了一會兒 gc 垃圾回收失敗,直接拋出問題終止 node 進程。

> node
Welcome to Node.js v12.9.1.
Type ".help" for more information.
> function foo() {
...   // 是否存在堆棧溢出錯誤?
...   return Promise.resolve().then(foo)
... }
undefined
> foo()
Promise { <pending> }


<--- Last few GCs --->

[6376:00000249F4E0C050]    56309 ms: Scavenge 1826.1 (2065.9) -> 1820.9 (2070.2) MB, 24.4 / 1.2 ms  (average mu = 0.138, current mu = 0.041) allocation failure
[6376:00000249F4E0C050]    58118 ms: Mark-sweep 1830.5 (2070.2) -> 1824.0 (2069.2) MB, 1742.0 / 33.9 ms  (average mu = 0.114, current mu = 0.088) allocation failure scavenge might not succeed

參考

李兵老師的圖解 Google V8-堆和棧:函數調用時如何影響到內存佈局的,筆者纔對棧溢出有了更深的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章