V8如何执行一段JavaScript代码
什么是V8?
一个由Google开发的开源JavaScript引擎,用于谷歌浏览器和Node.js中,其核心是执行人类易于理解的JavaScript代码。
执行过程主要分为两部,编译和执行。首先将JavaScript代码转换为低级中间代码或机器代码,再执行转化后的代码得出结果。
V8可以认为是一个虚拟的计算机,用来编译和执行JavaScript代码。虚拟机通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。
高级代码为什么需要先编译再执行?
首先,计算机只能执行二进制的指令。
为了能完成复杂的任务,工程师们为 CPU 提供了一大堆指令,来实现各种功能。我们把这一大堆指令称为指令集(Instructions),也就是机器语言。
CPU只能识别二进制指令,但对于程序员来说,二进制代码难以理解。所有有了更高级的汇编指令集,但CPU无法直接识别汇编语言,不同的CPU有着不用的指令集,而且编写汇编代码时还需要了解和处理器架构相关的硬件知识,所以我们有了更加高级的代码语言,如JavaScript等。
执行高级语言基本有两种形式:
一种是解释执行,将源代码通过解析器编译为中间代码,直接使用解释器执行中间代码,然后直接输出结果。如Java和Javascript。
一种是编译执行,将源代码转化为中间代码,再使用编译器再将中间代码编译成机器代码,以二进制文件形式存储,需要执行时,直接运行二进制文件。如C语言。
即便是 JavaScript 一门语言,也有好几种流行的虚拟机,它们之间的实现方式也存在着一部分差异,比如苹果公司在 Safari 中就是用 JavaScriptCore 虚拟机,Firefox 使用了 TraceMonkey 虚拟机,而 Chrome 则使用了 V8 虚拟机。
V8如何执行JavaScript代码?
V8使用的是混合编译执行和解释执行这两种手段。我们称为JIT技术(Just In Time)。
这是一种权衡策略,因为解释执行启动速度快,但执行速度慢;编译执行而启动速度慢,但执行速度快。
V8在执行之前,准备了JavaScript执行的一些基础环境,包括堆栈空间,全局上下文,全局作用域,内置函数,事件循环系统等。
- 全局上下文包含了执行过程过程中的全局信息,包括内置函数,全局变量等信息。
- 全局作用域包含了一些全局变量,在执行过程中,的数据都需要存放在内存中。作用域是用来查找变量的。
- V8采用栈和堆的内存管理方式,所以还需要初始化堆栈的结构
- 另外,初始化消息循环系统,消息循环包含了消息驱动器和消息队列。它犹如V8的心脏,不断的接受信息和决策如何处理消息。
源代码对于V8来说只不过是一堆字符串,我们需要去结构化这些字符,生成AST树,AST是便于V8理解的结构。另外,在生成AST的同时,V8还会生成相关的作用域,作用域中存放相关变量。
有了AST和作用域之后,接下来就可以生成字节码了。字节码是基于AST和机器代码之间的总价代码,与特定类型的机器代码无关,解释器可以直接解释执行字节码,或者通过编译器将其转译为二进制的机器代码再运行。
可以注意到,在解释器附近有一个监控机器人。这是一个监控解释器执行状态的模块,当在解释执行字节码的过程中,如果发现某段代码被重复执行和很多次,那么就会被标记为热点代码。之后就会被交给编译器把字节码转化为二进制代码,然后都编译后的二进制代码进行优化操作,优化后的二进制代码执行效率会大幅度提升。 接下来再执行这段代码时,V8会优先选择优化之后的二进制代码。
但由于JavaScript是一门动态语言,对象的结构和属性是可以在运行时任意修改的,而经过优化编译器优化过的代码只能针对某种固定的结构,一旦在执行过程中,对象的结构被动态修改了,那么优化后的代码就成了无效代码。这样子优化编译器就需要执行反优化,经过反优化的代码,下次执行时就会回退到解释器解释运行。
跟踪一段实际代码的执行流程
var test = 'merlin'
d8 --print-ast ./src/test.js 查看AST结构
[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (0x7fadec01be50) (mode = VAR, assigned = true) "test"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 11
. . . INIT at 11
. . . . VAR PROXY unallocated (0x7fadec01be50) (mode = VAR, assigned = true) "test"
. . . . LITERAL "merlin"
d8 --print-scopes ./src/test.js 查看作用域
Global scope:
global { // (0x7fcff6825230) (0, 494)
// will be compiled
// NormalFunction
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fcff6825540) local[0]
// local vars:
VAR test; // (0x7fcff6825450)
}
d8 --print-bytecode test.js 查看字节码
[generated bytecode for function: (0x1a270025376d <SharedFunctionInfo>)]
Bytecode length: 18
Parameter count 1
Register count 3
Frame size 24
Bytecode age: 0
0x1a27002537f6 @ 0 : 13 00 LdaConstant [0]
0x1a27002537f8 @ 2 : c3 Star1
0x1a27002537f9 @ 3 : 19 fe f8 Mov <closure>, r2
0x1a27002537fc @ 6 : 65 58 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
0x1a2700253801 @ 11 : 13 01 LdaConstant [1]
0x1a2700253803 @ 13 : 23 02 00 StaGlobal [2], [0]
0x1a2700253806 @ 16 : 0e LdaUndefined
0x1a2700253807 @ 17 : a9 Return
Constant pool (size = 3)
0x1a27002537c1: [FixedArray] in OldSpace
- map: 0x1a2700002239 <Map>
- length: 3
0: 0x1a27002537b5 <FixedArray[1]>
1: 0x1a270025374d <String[6]: #merlin>
2: 0x1a27001c2d1d <String[4]: #test>
Handler Table (size = 0)
Source Position Table (size = 0)
生成字节码之后,解释器会解释执行这段字节码,如果重复执行了某段代码,监控器就会将其标记为热点代码,并提交给编译器优化执行,如果你想要查看那些代码被优化了,可以使用下面的命令:d8 --trace-opt test.js
。如果要查看那些代码被反优化了,可以使用如下命令行来查看:pt --trace-deopt test.js