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-堆和栈:函数调用时如何影响到内存布局的,笔者才对栈溢出有了更深的了解。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注