浅析V8引擎,让你更懂JavaScript!
导语 | 本文介绍了编译、解释、动静态语言等基本概念,以及V8引擎的基本流程。本文将对其进行详细阐述,希望为更多的开发者提供经验和帮助。
一、编译与解释
二进制指令就是机器码:
编译:将源代码一次性转换成目标代码的过程。执行编译过程的程序叫编译器(Compiler)。
解释:将源代码逐条转换成目标代码,同时逐条运行的过程。执行解释过程的程序叫解释器(Interpreter)。解释器一般来说就是vm,vm有两种,一种是基于堆栈,一种是基于寄存器。
编译过程大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件等五个步骤,期间涉及到复杂的算法和硬件架构。解释器与此类似。
二、静态语言与动态语言
高级语言按照执行方式的不同,可分为静态语言和动态语言。
静态语言:使用编译执行的语言,如C、C++、Golang等。使用编译器一次性生成目标代码,“一次编译,无限次运行”,程序运行速度更快。编译型语言一般是不能跨平台的,也就是不能在不同的操作系统之间随意切换。
动态语言:使用解释执行的语言,如Python、Javascript、PHP等。执行过程中需要源代码,只要存在解释器,源代码可以在任何操作系统上运行,可移植性好,“一次编写,到处运行”。
解释型语言之所以能够跨平台,是因为有了解释器这个中间层。在不同的平台下,解释器会将相同的源代码转换成不同的机器码,解释器帮助我们屏蔽了不同平台之间的差异。
java和C#是一种比较奇葩的存在,它们是半编译半解释型的语言,源代码需要先转换成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中执行。Java引领了这种风潮,它的初衷是在跨平台的同时兼顾执行效率;C#是后来的跟随者,但是C#一直止步于Windows平台,在其它平台鲜有作为。
总结:
三、V8引擎
Javascript是解释型语言,那么V8引擎就对应着解释器。但是V8引擎为了提高JS的运行效率,会提前编译。
也就是V8引擎包括两个阶段:编译、执行,编译阶段指V8将JavaScript转换为字节码或者二进制机器码,执行阶段指解释器解释执行字节码,或者CPU直接执行二进制机器码。
(一)JIT
V8引擎同时采用了解释执行和编译执行这两种方式,也就是在运行时进行编译,这种方式称为JIT (Just in Time) 即时编译。
V8在执行JavaScript源码时,会先通过解析器将源码解析成AST,解释器会将AST转化为字节码,一边解释一遍执行。
解释器同时会记录某一代码片段的执行次数,如果执行次数超过了某个阈值,这段代码便会被标记为热代码(Hot Code),同时将运行信息反馈给优化编译器TurboFan,TurboFan根据反馈信息,会优化并编译字节码,最后生成优化的机器码。
(二)Parser生成抽象语法树
Parser生成AST抽象语法树过程包括语法分析、词法分析,和Babel等工具差不多。
生成AST中的一个优化是惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅执行时间过长,而且会消耗更多的内存。
惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(Pre-Parser),当函数被调用时,才会对其完全解析。
预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成AST,这项工作由Pre-Parser预解析器完成。
(三)Ignition生成字节码
字节码是机器码的抽象,可以看作是小型的构建块。相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度。
另外,字节码与特定类型的机器码无关,通过解释器将字节码转换为机器码后才可以执行,这样也使得V8更加方便的移植到不同的CPU架构。
可以通过如下命令,查看JavaScript代码生成的字节码。
node --print-bytecode index.js
注意,解释器执行字节码前,还是会将字节码转为机器码,因为计算机只识别机器码。
(四)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
}
四、总体流程
参考资料:
1.v8
2.编译型语言和解释型语言的区别
3.编译器与解释器的区别
4.js引擎能做到多小
5.深入理解JS引擎
6.V8是如何执行JavaScript代码的
7.JIT为什么能大幅度提升性能
8.JIT(just-in-time)即时编译
作者简介
杨国旺
腾讯前端开发工程师
腾讯前端开发工程师,欢迎讨论前端问题。
推荐阅读