# 一、开始

本文介绍了编译、解释、动静态语言等基本概念,以及 V8 引擎的基本流程。

# 二、编译与解释

二进制指令就是机器码

  • 编译:将源代码一次性转换成目标代码的过程。执行编译过程的程序叫编译器(Compiler)。
  • 解释:将源代码逐条转换成目标代码,同时逐条运行的过程。执行解释过程的程序叫解释器(Interpreter)。解释器一般来说就是 vm,vm 有两种,一种是基于堆栈,一种是基于寄存器。

编译过程大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件等五个步骤,期间涉及到复杂的算法和硬件架构。解释器与此类似。

# 三、静态语言与动态语言

高级语言按照执行方式的不同,可分为静态语言和动态语言。

  • 静态语言:使用编译执行的语言,如 C、C++、Golang 等。使用编译器一次性生成目标代码,“一次编译,无限次运行”,程序运行速度更快。编译型语言一般是不能跨平台的,也就是不能在不同的操作系统之间随意切换。
  • 动态语言:使用解释执行的语言,如 Python、Javascript、PHP 等。执行过程中需要源代码,只要存在解释器,源代码可以在任何操作系统上运行,可移植性好,“一次编写,到处运行”。

解释型语言之所以能够跨平台,是因为有了解释器这个中间层。在不同的平台下,解释器会将相同的源代码转换成不同的机器码,解释器帮助我们屏蔽了不同平台之间的差异。

java 和 C# 是一种比较奇葩的存在,它们是半编译半解释型的语言,源代码需要先转换成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中执行。Java 引领了这种风潮,它的初衷是在跨平台的同时兼顾执行效率;C# 是后来的跟随者,但是 C# 一直止步于 Windows 平台,在其它平台鲜有作为。

总结一下:

类型 原理 优点 缺点
编译型语言 通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码(以可执行文件的形式存在)。 编译一次后,脱离了编译器也可以运行,并且运行效率高 可移植性差,不够灵活。
解释型语言 由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码。 跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码。 一边执行一边转换,效率很低

# 四、V8引擎

Javascript 是解释型语言,那么 V8 引擎就对应着解释器。但是 V8 引擎为了提高 JS 的运行效率,会提前编译。

也就是 V8 引擎包括两个阶段:编译、执行,编译阶段指 V8 将 JavaScript 转换为字节码或者二进制机器码,执行阶段指解释器解释执行字节码,或者 CPU 直接执行二进制机器码。

# 1. JIT

V8 引擎同时采用了解释执行和编译执行这两种方式,也就是在运行时进行编译,这种方式称为 JIT (Just in Time) 即时编译。

V8 在执行 JavaScript 源码时,会先通过解析器将源码解析成 AST,解释器会将 AST 转化为字节码,一边解释一遍执行。

解释器同时会记录某一代码片段的执行次数,如果执行次数超过了某个阈值,这段代码便会被标记为热代码(Hot Code),同时将运行信息反馈给优化编译器 TurboFan,TurboFan 根据反馈信息,会优化并编译字节码,最后生成优化的机器码。

# 2. Parser 生成抽象语法树

Parser 生成 AST 抽象语法树过程包括语法分析、词法分析,和 Babel 等工具差不多。

生成 AST 中的一个优化是惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅执行时间过长,而且会消耗更多的内存。

惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(Pre-Parser),当函数被调用时,才会对其完全解析。 预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成 AST,这项工作由 Pre-Parser 预解析器完成。

# 3. Ignition 生成字节码

字节码是机器码的抽象,可以看作是小型的构建块。相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度。

另外,字节码与特定类型的机器码无关,通过解释器将字节码转换为机器码后才可以执行,这样也使得 V8 更加方便的移植到不同的 CPU 架构。

可以通过如下命令,查看 JavaScript 代码生成的字节码。

node --print-bytecode index.js

注意,解释器执行字节码前,还是会将字节码转为机器码,因为计算机只识别机器码。

# 4. TurboFan

Ignition 执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan。 然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。 另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到 Ignition。

TurboFan 做的优化包括内联(inlining)和逃逸分析(Escape Analysis)。

内联就是将相关联的函数进行合并,减少运行时间。比如:

function add(a, b) {
  return a + b
}
function foo() {
  return add(2, 4)
}

内联处理后:

function fooAddInlined() {
  var a = 2
  var b = 4
  var addReturnValue = a + b
  return addReturnValue
}

// 因为 fooAddInlined 中 a 和 b 的值都是确定的,所以可以进一步优化
function fooAddInlined() {
  return 6
}

逃逸分析就是分析对象的生命周期是否仅限于当前函数,如果是的话会对其进行优化。比如:

function add(a, b){
  const obj = { x: a, y: b }
  return obj.x + obj.y
}

会处理成:

function add(a, b){
  const obj_x = a
  const obj_y = b
  return obj_x + obj_y
}

# 5. 总体流程

# 五、相关资料

  1. v8 (opens new window)
  2. 编译型语言和解释型语言的区别 (opens new window)
  3. 编译器与解释器的区别 (opens new window)
  4. js引擎能做到多小? (opens new window)
  5. 深入理解JS引擎 (opens new window)
  6. V8是如何执行JavaScript代码的? (opens new window)
  7. JIT 为什么能大幅度提升性能? (opens new window)
  8. JIT(just-in-time) 即时编译 (opens new window)