最近看到一篇文章,详细讲述了浏览器是如何工作的,感觉非常好,所以决定一点点摘录及研究下。

  V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_字节码

一、为什么需要 JavaScript 引擎

  我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码

  JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收



# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000 #机器指令
mov ax,bx #汇编指令


1、热门 JavaScript 引擎

  • ​V8 (Google)​​,用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
  • ​JavaScriptCore (Apple)​​,开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
  • Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit
  • ​SpiderMonkey (Mozilla)​​,第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
  • Chakra (JScript 引擎),用于 Internet Explorer。
  • Chakra (JavaScript 引擎),用于 Microsoft Edge。
  • KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波顿开发,用于 KDE 项目的 Konqueror 网页浏览器中。
  • JerryScript — 三星推出的适用于嵌入式设备的小型 JavaScript 引擎。
  • 其他:Nashorn、​​QuickJS​​ 、 ​​Hermes​

 2、V8

  ​​Google V8​​ 引擎是用 C ++编写的开源高性能 JavaScript 和 ​​WebAssembly​​ 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。

   V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版​​Chrome​​于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。

  和​​其他 JavaScript 引擎​​一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_寄存器_02

资料拓展:​​v8 logo​​ | ​​V8 (JavaScript engine)​​) | ​​《V8、JavaScript+的现在与未来》​​ | ​​几张图让你看懂 WebAssembly​

V8一词最早见于“V-8 engine”,即​​V8发动机​​,一般使用在中高端车辆上。8个气缸分成两组,每组4个,成V型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar和NASCAR都要求使用V8发动机。

3、什么是 D8

  d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息

V8源码编译出来的可执行程序名为d8。d8作为V8引擎在命令行中可以使用的交互shell存在。Google官方已经不记得d8这个名字的由来,但是作为"delveloper shell"的缩写,用首字母d和8结合,恰到好处。还有一种说法是d8最初叫​​developer shell​​,因为d后面有8个字符,因此简写为d8,类似于​​i18n​​(internationalization)这样的简写。参考:​​Using d8​

二、V8 引擎的内部结构

  V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:

1、​​Parser​​:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)

  确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_寄存器_03

2、​​Ignition​​:interpreter,即解释器

  负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。

  通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based)

  基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;

  基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。

  通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。

  基于寄存器的解释器架构:

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_javascript_04

资料参考:​​解释器是如何解释执行字节码的?​

3、​​TurboFan​​:compiler,即编译器,

  利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;

4、​​Orinoco​​:garbage collector,垃圾回收模块

  负责将程序不再需要的内存空间回收。

  其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_字节码_05

  简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能。

  图片中的红色虚线是逆向的,也就是说Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。



function add(x, y) {
return x + y;
}

add(3, 5);
add('3', '5');


  在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation)简称为 JIT。因此,V8 也属于 JIT 编译器

资料拓展参考:​​V8 引擎是如何工作的?​

三、V8 是怎么执行一段 JavaScript 代码的

 1、在 V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因

  而 V8 率先引入了即时编译(JIT)双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。

  V8 出现之后,各大厂商也都在自己的 JavaScript 虚拟机中引入了 JIT 机制,所以目前市面上 JavaScript 虚拟机都有着类似的架构。另外,V8 也是早于其他虚拟机引入了惰性编译、内联缓存、隐藏类等机制,进一步优化了 JavaScript 代码的编译执行效率

2、V8 执行一段 JavaScript 的流程图:

浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行_寄存器_06

资料拓展:​​V8 是如何执行一段 JavaScript 代码的?​

3、V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:

  • 第一种是将高级代码转换为二进制代码,再让计算机去执行;
  • 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。

 4、解释执行和编译执行都有各自的优缺点:

  解释执行启动速度快,但是执行时速度慢,

  而编译执行启动速度慢,但是执行速度快

  为了充分地利用解释执行和编译执行的优点,规避其缺点:

  V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码

5、总结:

  V8 执行一段 JavaScript 代码所经历的主要流程包括:

(1)初始化基础环境;

(2)解析源码生成 AST 和作用域;

(3)依据 AST 和作用域生成字节码;

(4)解释执行字节码;

(5)监听热点代码;

(6)优化热点代码为二进制的机器代码;

(7)反优化生成的二进制机器代码。