Skip to main content

V8编译流程

运行时环境

在执行 JavaScript 代码之前,V8 已经准备好了代码的运行时环境,包括全局执行上下文,全局作用域,堆空间和栈空间,内置的内建函数,宿主环境提供的扩展函数,消息循环系统。

宿主环境

浏览器和 Node.js 都是 V8 的宿主环境,其关系如下:

栈空间和堆空间

Chrome 中打开一个渲染进程,渲染进程就会初始化 V8,同时初始化栈空间和堆空间

栈空间主要用来管理 JavaScript 函数的调用的。可以简单理解为函数的执行上下文存放在栈空间中,具体的如基本类型的数据,引用对象的地址,函数执行状态,this等都存放在栈空间中。

占用内存比较大,或者不需要连续存储的数据则会放在堆空间,如引用类型的数据,window对象,document对象等。

全局执行上下文和全局作用域

V8 开始执行一段代码时,会生成一个执行上下文。执行上下文维护了当前执行代码所需的变量声明、this指向等。

作用域是一个抽象概念,规定了变量的查找顺序,一个执行上下文中,可能会有多个作用域。

构造事件循环系统

深入理解 JavaScript Event Loop

CPU执行机器代码

解释执行和编译后执行用到了堆和栈

解析代码

V8 采用的是惰性解析策略:解析器在解析代码时,遇到函数声明,会跳过函数内部代码,不会为其生成AST和字节码。

V8 处理闭包的策略是:在父函数执行时,虽然解析器不执行子函数,但会使用预解析器来判断当前子函数是否引用了内部变量,如果引用了,就会将栈中的变量复制到堆中,当父函数执行结束被销毁后,执行子函数时,就会使用堆中的变量。

字节码

早期,V8 是将代码直接编译成二进制的机器代码,然后执行的。但是随着移动设备的普及,这种方式存在两个问题:

  1. 编译时间过久,影响代码启动速度;
  2. 缓存编译后的二进制代码占用的空间大;(缓存二进制代码是为了重用,省去了再次编译的过程)

新的 V8 引入了字节码、解释器(lgnition)、优化编译器(turboFan):

  1. 解析生成字节码的时间很短
  2. 字节码占用空间小,缓存字节码降低内存占用
  3. 采用字节码,可以简化程序复杂度,编译成不同CPU架构的机器码更方便。

JavaScript 代码被解析器(Parser)解析为AST,同时生成作用域;然后AST被传给解释器 lgnition 的字节码生成器(BytecodeGenerator),生成以函数为单位的字节码。

字节码和汇编代码类似,每一行表示一个功能。

提升 V8 查找对象属性的效率

快属性和慢属性

隐藏类

隐藏类是利用了静态语言的特点来查找对象属性

V8 中,每个对象都有一个隐藏类,称为 map;

每个对象的第一个属性的指针都指向其 map 的地址;

map 描述了对象的内存布局,包括对象的有哪些属性,每个属性的相对于对象的偏移量

如果添加新属性或删除属性,隐藏类都会重新构建。

内联缓存

V8 在执行函数的过程中,会观察函数中的一些调用点(callSite)上的关键的中间数据,然后将这些数据缓存起来,当再次执行该函数时,V8 就可以直接使用这些缓存的数据。

这些中间数据包括加载对象属性(Load)、给对象属性赋值(Store)、函数调用(Call),V8 将这些监听的数据存放在一个称为反馈向量(FeedBack Vector)的表结构中。每个函数都会维护一个反馈向量。