V8编译流水线-函数调用

898 阅读3分钟

解释执行和直接执行二进制代码都使用了堆栈结构。那为什么使用栈结构管理函数调用?

1. 为什么使用栈管理函数调用

通常函数有两个主要的特性:

  • 函数可以被调用。当函数调用发生时,执行代码的控制权将从父函数转移到子函数,子函数执行结束后,又会将代码控制权返还给父函数。
  • 函数具有作用域机制。函数在执行时可以将定义在函数内部的变量和外部环境隔离,函数内部定义的变量外部无法访问到,且函数执行结束后,内部变量也会被销毁。

函数调用者(父函数)的生命周期总是长于被调用者(子函数),并且被调用者(子函数)的生命周期总是先于调用者(父函数)的生命周期结束。在函数资源分配和回收角度看,被调用函数(子函数)的资源分配总是晚于调用函数(父函数),且资源释放也先于调用函数(父函数)。是一种后进先出LIFO的策略,栈结构也是这种模式,所以用栈来管理函数调用关系。

2. 栈如何管理函数调用

  • x=5,变量x第一次压入到栈中。
  • y=6,变量y第一次压入到栈中。
  • x=100,替换之前压入栈的x的值,x的值由5改为100.
  • 计算x+y的值,赋值给z,并压入到栈中。

函数在执行过程中,其内部变量会按照执行顺序被压入到栈中。当父函数内嵌子函数时,子函数调用结束,就会把函数执行权交还给父函数,这个恢复的过程叫恢复现场。

恢复现场使用的方法就是在寄存器中保存一个永远指向当前栈顶的指针,栈顶指针的作用就是告诉你往哪个位置添加新元素,这个指针通常存放在esp寄存器中。同时增加另一个ebp寄存器,用来保存当前函数(父函数)的起始位置,这个位置叫栈帧指针。

每个栈帧对应一个未运行完的函数,栈帧中保存了该函数的返回地址和局部变量。在JS中,函数执行过程也是类似的。调用一个新函数,v8会为该函数创建栈帧,等函数执行结束之后,销毁该栈帧。而栈结构的容量是固定的,如果不销毁,很容易导致栈溢出。

3. 堆的作用

栈的缺点是在内存中不能分配一块连续的较大的空间,所以栈空间是有限的。此时就有了堆空间,用来保存一些大数据。

堆空间中的数据不要求连续存放,从堆上分配内存没有固定模式,可以在任何时候分配和释放它。当遇到大数据时,会在堆中分配一块空间,返回分配后的内存地址,该地址会被保存在栈中。比如下图中的pp,栈中的地址指向了在堆中分配的空间地址。

当堆中的数据不再需要的时候,需要对其进行销毁。如果不及时销毁,容易造成内存泄漏。

4. 总结

  • 用栈结构管理函数的调用过程,称为调用栈。
  • 栈有最大容量限制,容易造成栈溢出。所以使用堆来存取大数据,然后在栈中保存堆的引用地址。
  • 解决栈溢出,也可以将同步函数拆分成异步函数处理。

写在最后

V8相关的学习总结来自于极客时间李兵老师的课程《图解goole V8》,如果想了解更多细节,可以进课程查看。