V8是如何执行JavaScript代码的?

2,087 阅读7分钟

v8与javascript

介绍

V8 是一个由 Google 开发的开源 JavaScript(解释行语言) 引擎,用C++(编译型语言)编写,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。由于V8引擎在JavaScript性能优化方面做了很大的提升,所以也让他成为了大众喜爱的开源高性能JavaScript引擎。

\

\

背景

Google (丹麦)研发小组在 2006 年开始研发 V8 ,部分的原因是 Google 对既有 JavaScript 引擎的执行速度不满意, 在2008年推出chrome, 巨大的速度优势, 迅速占领市场. 2017年chrome的市场占有达到59%.

应用

Chrome浏览器JS的引擎是V8
Nodejs的运行环境是V8引擎
electron的底层引擎也是V8

原因

这里先说一下什么是编译型语言和解释性语言:

编译型语言: 在程序执行之前必须进行专门的编译过程,有如下特点:

  • 只须编译一次就可以把源代码编译成机器语言,后面的执行无须重新编译,直接使用之前的编译结果就可以;因此其执行的效率比较高;
  • 编译性语言代表:C、C++、Java、Pascal/Object Pascal(Delphi);

程序执行效率比较高,但比较依赖编译器,因此跨平台性差一些;

  • 不同平台对编译器影响较大。
  • 16位系统下int是2个字节(16位),而32位系统下int占4个字节(32位);
  • 32位系统下long类型占4字节,而64位系统下long类型占8个字节;

解释性语言 - 解释行语言,支持动态类型,弱类型,在程序运行的时候才进行编译,而编译前需要确定变量的类型,效率比较低,对不同系统平台有较大的兼容性.

  • 源代码不能直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行;
  • 源代码—>中间代码—>机器语言
  • 程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次;
  • 解释性语言代表:Python、JavaScript、Shell、Ruby、MATLAB等;
  • 运行效率一般相对比较低,依赖解释器,跨平台性好;

比较:

  • 一般,编译性语言的运行效率比解释性语言更高;但是不能一概而论,部分解释性语言的解释器通过在运行时动态优化代码,甚至能使解释性语言的性能超过编译性语言;
  • 编译性语言的跨平台特性比解释性语言差一些;

进过以上说明,解释性语言,运行效率低,随着Web相关技术的发展,JavaScript所要承担的工作也越来越多,早就超越了“表单验证”的范畴,这就更需要快速的解析和执行JavaScript脚本。V8引擎就是为解决这一问题而生,在node中也是采用该引擎来解析JavaScript。

编程语言是如何运行的

众所周知,我们通过编程语言完成的程序是通过处理器运行的。但是处理器不能直接理解我们通过高级语言(如C++、Go、JavaScript等)编写的代码,只能理解机器码,所以在执行程序之前,需要经过一系列的步骤,将我们编写的代码翻译成机器语言。这个过程一般是由编译器(Compiler) 或者解释器(Interpreter) 来完成。

那么编译器和解释器的工作流程是怎样的呢?

从上图可以看出它们的大概的工作流程。那么既然编译器和解释器都可以完成代码翻译的工作,为何还同时存在呢?

这是因为编程语言有两个类别:静态类型和动态类型。静态类型的语言,比如C++、Go等,都需要提前编译 (AOT) 成机器码然后执行,这个过程主要使用编译器来完成;而动态语言,比如JavaScript、Python等,只在运行时进行编译执行 (JIT) ,这个过程通过解释器完成。

通过上面的描述,我们已经知道了JavaScript是通过解释器来进行翻译执行的,那么JavaScript引擎V8执行Js代码的详细过程是怎么样的呢?接下来我们详细分析一下。

\

V8执行Js代码的整体流程

在这个过程中,V8同时使用了Parser(解析器)Ignition(解释器)TurboFan(优化编译器) 来执行Js代码。

当v8执行javascript源码时候,解析器会把源码解析成抽象语法树(AST),然后解释器再将AST翻译成字节码,一边解释,一边执行。在此过程中,解释器会记特定的代码片段的运行次数,如果运行次数超过了某个阈值,该段代码会被标记为热代码。并将运行信息反馈给优化编译器。优化编译器根据反馈信息,优化并编译字节码。最终生成优化后的机器码,当该段代码继续执行,解释器就直接使用优化机器代码执行,不用再次解释,从而大大提高了代码运行效率,这种在运行时编译代码的技术,也被称为JIT(即时编译)。通过JIT可以极大提升JavaScript代码的执行性能。

1.Parser生成抽象语法树

在Chrome中开始下载Javascript文件后,Parser就会开始并行在单独的线程上解析代码。这意味着解析可以在下载完成后仅几毫秒内完成,并生成AST。

\

上图是一段Js代码转成AST后的结构图,从图中可以看出AST是把代码结构化成树状结构表示,这样做是为了更好的让编译器或者解释器理解。此外,AST还广泛应用于各类项目中,比如Babel、ESLint,那么AST的生成过程是怎么样的呢?

1. 词法分析(lexical analysis) :主要是将字符流(char stream) 转换成标记流(token stream) ,字符流就是我们一行一行的代码,token是指语法上不能再分的、最小的单个字符或者字符串。

var name = "ivweb"
//转成token后为

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "name"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "String",
        "value": ""ivweb""
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

\

从上面可以看出,var name = "ivweb"; 这样一段代码,会有关键字"var"、标识符"name"、赋值运算符"="、字符串"ivweb"、分隔符";",共5个token。
2. 语法分析:将前面生成的token流根据语法规则,形成一个有元素层级嵌套的语法规则树,这个树就是AST。在此过程中,如果源代码不符合语法规则,则会终止,并抛出“语法错误”。

2.Ignition生成字节码

\

字节码是机器码的抽象,可以看作是小型的构建块,这些构建块组合到一起构成任何JavaScript功能。字节码比机器码占用更小的内存,这也是为什么V8使用字节码的一个很重要的原因。字节码不能够直接在处理器上运行,需要通过解释器将其转换为机器码后才能执行。

\

通过上图可以看出,Ignition把前一步得到的AST通过字节码生成器经过一些列的优化生成字节码。
在这个过程中:

  • Register Optimizer: 主要是避免寄存器不必要的加载和存储;
  • Peephole Optimizer: 寻找直接码中可以复用的部分,并进行合并;
  • Dead-code Elimination: 删除无用的代码,减少字节码的大小

通过上面三个过程的优化进一步减小字节码的大小并提高性能,最后Ignition执行优化后的字节码。

3.执行代码及优化

\

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