Skip to main content

01 V8如何执行一段JavaScript代码

本节是对 V8 做一个宏观的、全面的介绍,让你对 V8 的执行流程有个整体上的认识。

什么是 V8

V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。

主要核心流程分为编译和执行两步。首先需要将 JavaScript 代码转换为低级中间代码或者机器能够理解的机器代码,然后再执行转换后的代码并输出执行结果。

高级代码为什么需要先编译再执行

为 CPU 提供二进制代码指令,来实现各种功能,这些指令称为指令集(Instructions),也就是机器语言。

CPU 只能识别二进制的指令,但对程序员来说,二进制代码难以阅读和记忆,将二进制指令集转换为人类可以识别和记忆的符号,这就是汇编指令集。

1000100111011000  机器指令
mov ax,bx 汇编指令

CPU 不能直接识别汇编语言,需要一个汇编编译器,将汇编代码编程成机器代码。

  • 不同的 CPU 有着不同的指令集,使用汇编语言来实现一个功能,需要为每种架构的 CPU 编写特定的汇编代码。
  • 在编写汇编代码时,需要了解和处理器架构相关的硬件知识。

高级语言:屏蔽计算机架构细节的语言,能适应多种不同 CPU 架构的语言,能专心处理业务逻辑的语言,诸如 C、C++、Java、C#、Python、JavaScript 等。

解释执行:先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果。

编译执行:先将源代码转换为中间代码,然后编译器再将中间代码编译成机器代码。通常机器代码是以二进制文件形式存储的,直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,直接执行内存中的二进制代码。

执行 JavaScript 需要经过 JavaScript 虚拟机的转换。目前几种流行的虚拟机,实现方式存在着一部分差异,苹果公司在 Safari 中使用 JavaScriptCore 虚拟机,Firefox 使用 TraceMonkey 虚拟机,Chrome 使用了 V8 虚拟机。

V8 是怎么执行 JavaScript 代码的

V8 采用混合编译执行和解释执行两种方式,称为 JIT(Just In Time)技术,一种权衡策略,解释执行的启动速度快,但执行时的速度慢,而编译执行的启动速度慢,但执行时的速度快。

完整的 V8 执行 JavaScript 的流程图:

V8 启动执行 JavaScript 前,需要准备执行 JavaScript 时所需要的一些基础环境,包括了“堆空间”“栈空间”“全局执行上下文”“全局作用域”“消息循环系统”“内置函数”等,这些内容都是在执行 JavaScript 过程中需要使用到的。

  • JavaScript 全局执行上下文:包含执行过程中的全局信息(一些内置函数,全局变量等信息);
  • 全局作用域:包含一些全局变量(在执行过程中的数据都需要存放在内存中);
  • 初始化内存中的堆和栈结构:V8 是采用了经典的堆和栈的内存管理模式;
  • 消息循环系统:包含了消息驱动器和消息队列,不断接受消息并决策如何处理消息。

基础环境准备好后,可以向 V8 提交要执行的 JavaScript 代码了。

  1. V8 接收到要执行的 JavaScript 源代码,此时是一堆字符串;
  2. 结构化这段字符串,生成抽象语法树 (AST),便于 V8 理解。在生成 AST 的同时,V8 还会生成相关的作用域,作用域中存放相关变量;(结构化:信息经过分析后可分解成多个互相关联的组成部分,各组成部分间有明确的层次结构,方便使用和维护,并有一定的操作规范)。
  3. 生成字节码,字节码是介于 AST 和机器代码的中间代码。与特定类型的机器代码无关,解释器可以直接解释执行字节码,或者通过编译器将其编译为二进制的机器代码再执行;
  4. 解释器按照顺序解释执行字节码,并输出执行结果;(解释执行字节码的过程中,如果发现了某一段代码会被重复多次执行,监控机器人会将这段代码标记为热点代码。)
  5. V8 会将被标记为热代码的这段字节码丢给优化编译器,优化编译器会在后台将字节码编译为二进制代码,然后再对编译后的二进制代码执行优化操作,优化后的二进制机器代码的执行效率会得到大幅提升。后面再执行到这段代码时,V8 会优先选择优化之后的二进制代码,代码的执行速度就会大幅提升。

跟踪一段实际代码的执行流程

var test = "GeekTime";

被解析器结构化成 AST,可以使用 V8 提供的调试工具 D8 来查看,将上面那段代码保存到 test.js 的文件中,然后执行下面命令:

d8 --print-ast test.js

--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (0x7ff0e3022298) (mode = VAR, assigned = true) "test"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 11
. . . INIT at 11
. . . . VAR PROXY unallocated (0x7ff0e3022298) (mode = VAR, assigned = true) "test"
. . . . LITERAL "GeekTime"

AST 就是 JS 源代码的结构化表述,是一个树状结构:

使用 D8 看生成的作用域:

d8 --print-scopes test.js

Global scope:
global { // (0x7fd974022048) (0, 24)
// will be compiled
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fd9740223c8) local[0]
// local vars:
VAR test; // (0x7fd974022298)
}

使用解释器生成字节码:

d8 --print-bytecode test.js

[generated bytecode for function:  (0x2b510824fd55 <SharedFunctionInfo>)]
Parameter count 1
Register count 4
Frame size 32
0x2b510824fdd2 @ 0 : a7 StackCheck
0x2b510824fdd3 @ 1 : 12 00 LdaConstant [0]
0x2b510824fdd5 @ 3 : 26 fa Star r1
0x2b510824fdd7 @ 5 : 0b LdaZero
0x2b510824fdd8 @ 6 : 26 f9 Star r2
0x2b510824fdda @ 8 : 27 fe f8 Mov <closure>, r3
0x2b510824fddd @ 11 : 61 32 01 fa 03 CallRuntime [DeclareGlobals], r1-r3
0x2b510824fde2 @ 16 : 12 01 LdaConstant [1]
0x2b510824fde4 @ 18 : 15 02 02 StaGlobal [2], [2]
0x2b510824fde7 @ 21 : 0d LdaUndefined
0x2b510824fde8 @ 22 : ab Return
Constant pool (size = 3)
0x2b510824fd9d: [FixedArray] in OldSpace
- map: 0x2b51080404b1 <Map>
- length: 3
0: 0x2b510824fd7d <FixedArray[4]>
1: 0x2b510824fd1d <String[#8]: GeekTime>
2: 0x2b51081c8549 <String[#4]: test>
Handler Table (size = 0)
Source Position Table (size = 0)

解释器解释执行这段字节码,如果重复执行了某段代码,监控器就会将其标记为热点代码,并提交给编译器优化执行。

查看那些代码被优化了:

d8 --trace-opt test.js

查看那些代码被反优化了:

pt --trace-deopt test.js