10 堆和栈:函数调用是如何影响到内存布局的
在同一个任务中重复调用嵌套的 foo 函数,V8 报告栈溢出的错:
function foo() {
foo();
}
foo();
使用 setTimeout 让 foo 函数在不同的任务中执行,程序可以正确执行:
function foo() {
setTimeout(foo, 0);
}
在同一个任务中执行 foo 函数,但不是嵌套执行。没有报告栈溢出的错误,但会让整个页面卡住:
function foo() {
return Promise.resolve().then(foo);
}
foo();
V8 执行这三种不同代码时,内存布局不同,从而会影响到代码的执行逻辑。
为什么使用栈结构来管理函数调用
函数的两个主要特性:
- 可以被调用:可以在一个函数中调用另外一个函数,当函数调用发生时,执行代码的控制权将从父函数转移到子函数,子函数执行结束之后,又会将代码执行控制权返还给父函数;
- 具有作用域机制:指函数在执行的时候可以将定义在函数内部的变量和外部环境隔离,在函数内部定义的变量称为临时变量,临时变量只能在该函数中被访问,外部函数通常无权访问,当函数执行结束之后,存放在内存中的临时变量也随之被销毁。
int getZ()
{
return 4;
}
int add(int x, int y)
{
int z = getZ();
return x + y + z;
}
int main()
{
int x = 5;
int y = 6;
int ret = add(x, y);
}
- 当 main 函数调用 add 函数时,需要将代码执行控制权交给 add 函数;
- 然后 add 函数又调用了 getZ 函数,于是又将代码控制权转交给 getZ 函数;
- 接下来 getZ 函数执行完成,需要将控制权返回给 add 函数;
- 当 add 函数执行结束之后,需要将控制权返还给 main 函数;
- 然后 main 函数继续向下执行。
**函数调用者的生命周期总是长于被调用者(后进),并且被调用者的生命周期总是先于调用者的生命周期结束 (先出)。被调用函数的资源分配总是晚于调用函数 (后进),而函数资源的释放则总是先于调用函数 (先出)。它们都符合后进先出 (LIFO) **的策略,而栈结构正好满足这种后进先出 (LIFO) 的需求,所以选择栈来管理函数调用关系。
栈如何管理函数调用
当一个函数被执行时,函数的参数、函数内部定义变量都会依次压入到栈中:
函数在执行过程中,其内部的临时变量会按照执行顺序被压入到栈中。
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;
}
当执行到 int z = add(x,y) 时,当前栈的状态:
调用 add 函数,执行 add 函数的过程:
当 add 函数执行完成之后,需要将执行代码的控制权转交给 main 函数,需要将栈的状态恢复到 main 函数上次执行时的状态,这个过程叫恢复现场。
在寄存器中保存一个永远指向当前栈顶的指针,当 add 函数执行结束时,只需要将栈顶指针向下移动就可以了:
CPU 增加另外一个 ebp 寄存器,用来保存当前函数的起始位置,函数的起始位置也称为栈帧指针:
在 main 函数调用 add 函数的时候,main 函数的栈顶指针就变成了 add 函数的栈帧指针,需要将 main 函数的栈顶指针保存到 ebp 中,当 add 函数执行结束之后,需要销毁 add 函数的栈帧,并恢复 main 函数的栈帧,需要取出 main 函数的栈顶指针写到 esp 中即可,相当于将栈顶指针移动到 main 函数的区域。
setTimeout 会使得被调用的函数在消息队列后面的任务中执行,所以不会影响到当前的栈结构。Promise 会使得被调用的的函数在微任务中执行,导致主线的卡死。
既然有了栈,为什么还要堆
栈是连续的,在内存中分配一块连续的大空间非常难,因此栈空间是有限的。堆是另外一种数据结构用来保存一些大数据。存放在堆空间中的数据不要求连续存放,从堆上分配内存块没有固定模式,可以在任何时候分配和释放它。
struct Point
{
int x;
int y;
};
int main()
{
int x = 5;
int y = 6;
int *z = new int;
*z = 20;
Point p;
p.x = 100;
p.y = 200;
Point *pp = new Point();
pp->y = 400;
pp->x = 500;
delete z;
delete pp;
return 0;
}