浅析浏览器是如何工作的(一):V8引擎、JIT机制、JS代码解释执行与编译执行
最近看到一篇文章,详细讲述了浏览器是如何工作的,感觉非常好,所以决定一点点摘录及研究下。
V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行。
一、为什么需要 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 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“的过程,具体流程如下:
2、Ignition:interpreter,即解释器
负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。
通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),
基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;
基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。
通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系。大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
基于寄存器的解释器架构:
资料参考:解释器是如何解释执行字节码的?
3、TurboFan:compiler,即编译器,
利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
4、Orinoco:garbage collector,垃圾回收模块
负责将程序不再需要的内存空间回收。
其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
简单地说,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 的流程图:
3、V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
- 第一种是将高级代码转换为二进制代码,再让计算机去执行;
- 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。
4、解释执行和编译执行都有各自的优缺点:
解释执行启动速度快,但是执行时速度慢,
而编译执行启动速度慢,但是执行速度快。
为了充分地利用解释执行和编译执行的优点,规避其缺点:
V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
5、总结:
V8 执行一段 JavaScript 代码所经历的主要流程包括:
(1)初始化基础环境;
(2)解析源码生成 AST 和作用域;
(3)依据 AST 和作用域生成字节码;
(4)解释执行字节码;
(5)监听热点代码;
(6)优化热点代码为二进制的机器代码;
(7)反优化生成的二进制机器代码。
作者:独钓寒江雪
原文链接:https://segmentfault.com/a/1190000037435824