Skip to content
On this page

V8如何执行一段JavaScript代码

什么是V8?

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

执行过程主要分为两部,编译和执行。首先将JavaScript代码转换为低级中间代码或机器代码,再执行转化后的代码得出结果。

V8可以认为是一个虚拟的计算机,用来编译和执行JavaScript代码。虚拟机通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。

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

首先,计算机只能执行二进制的指令。

为了能完成复杂的任务,工程师们为 CPU 提供了一大堆指令,来实现各种功能。我们把这一大堆指令称为指令集(Instructions),也就是机器语言。

CPU只能识别二进制指令,但对于程序员来说,二进制代码难以理解。所有有了更高级的汇编指令集,但CPU无法直接识别汇编语言,不同的CPU有着不用的指令集,而且编写汇编代码时还需要了解和处理器架构相关的硬件知识,所以我们有了更加高级的代码语言,如JavaScript等。

执行高级语言基本有两种形式:

一种是解释执行,将源代码通过解析器编译为中间代码,直接使用解释器执行中间代码,然后直接输出结果。如JavaJavascript

一种是编译执行,将源代码转化为中间代码,再使用编译器再将中间代码编译成机器代码,以二进制文件形式存储,需要执行时,直接运行二进制文件。如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是一门动态语言,对象的结构和属性是可以在运行时任意修改的,而经过优化编译器优化过的代码只能针对某种固定的结构,一旦在执行过程中,对象的结构被动态修改了,那么优化后的代码就成了无效代码。这样子优化编译器就需要执行反优化,经过反优化的代码,下次执行时就会回退到解释器解释运行。

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

使用 jsvu 快速调试 v8

js
var test = 'merlin'
shell
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

MIT Licensed | Copyright © 2021 - 2022