V8如何执行JavaScript代码?

前言

封面是跟敖丙的日常桌球,丙丙球技很阔以!

JavaScript 我们每天都在写,那我们写的每一行代码是如何被执行的呢?

我们经常说 JavaScript 是解释型语言,那它是如何被解释的呢?


这篇围绕 V8 如何执行 JavaScript 展开,希望你看完,有这些收获:

  • 编译器、解释器有何区别

  • 什么是抽象语法树 AST

  • 什么是 JIT 技术

  • JavaScript 执行流程

编译器、解释器

首先需要明确,我们的代码是不能直接被 CPU 执行的,因为 CPU 只能识别二进制的指令,但是二进制的指令都是类似10111011111100110 这样的,很明显,如果我们去写一个程序,都是直接用二进制,难度可想而知有多大。

就算给你一张二进制指令映射表,告诉你每一串二进制对应指令的作用,对于工程师们而言,都是极其痛苦,以及容易出错的。

于是,汇编原因就这样产生了,我们把每一个复杂的二进制指令都映射成一个汇编指令,这样就大大减小了我们的记忆量,也让我们的代码变得可维护,可扩展。比如下面的汇编指令:

  • add 指令:用于将两个运算子相加,并将结果写入第一个运算子。

  • mov 指令:用于将一个值写入某个寄存器。

  • ……

虽然汇编语言,相对机器语言来讲,简单了不少,但不可否认的是,汇编语言仍然是绝大部分工程师不能接受的语言。

然后这个传奇般的男人,‘竟写过’极其优美的汇编代码,他究竟是何方神圣??

莫非每一位互联网大佬背后都有一段不为人知的逆天代码??

并非每个人都是雷老师,即使我们用汇编写一个比较简单的功能,也需要大量的汇编代码。可以从下面两个痛点来分析。

  • CPU 的种类很多,不同 CPU 的指令集又不一样,那么我们每实现一个功能都需要为每种不同的 CPU 写一套特定的汇编

  • 汇编直接对接机器指令,要想写好汇编,需要我们的计算机底子非常好,比如一些硬件知识,操作系统底层的一些东西,都需要掌握,不然很难写出高质量的汇编代码

然后现实就是,大部分的工程师,对底层并没有那么的了解,特别是在一线业务团队的小伙伴们,大部分的时间都 hold 在了业务逻辑上,根本没时间来搞这些底层的细节。

综上,汇编不是我们想要的!!

我们想要的是,能够解决上面痛点的语言,写起来让人焕然一新、神清气爽的那种。

于是乎,所谓的高级语言就产生了。比如 JavaScript、PHP、Java、Python 等都是高级语言。

同样的,这些高级语言也不能直接被 CPU 执行,跟汇编语言一样,也是需要进行转换。

有下面两种方式(编译执行 or 解释执行)来处理、执行这些高级语言。

编译执行

其实看图大概也知道了,编译执行首先需要将高级语言,通过解析器转换为中间代码,然后编译器再把中间代码编译成机器代码(通常是指二进制文件),然后 CPU 就可以直接执行了。

解释执行

解释执行相对编译执行,中间代码是直接由解释器进行执行,输出结果,而编译执行,需要先编译成机器代码。

鱼和熊掌不能兼得

通过上面的解释,其实比较容易看出编译执行跟解释执行各自的优缺点。

  • 编译执行启动速度会慢一些,但是执行速度会快一些。

  • 解释执行启动速度会快一些,但是执行速度会慢一些。

绕了一大圈,拉回主题,那我们的 V8 引擎究竟是采取的哪种方式处理的 JavaScript 呢?

莫慌莫慌,到点了,先听一首网抑云缓解一下最近股市大跌的沉重心情

V8执行流程-JIT(Just In Time)技术

V8 采用了一种权衡的策略,让编译执行跟解释执行打了个配合,完美!

先来一张现在 V8 执行 JavaScript 的流程图(其实早期的 V8 执行流程不是这样,这里不展开说,感兴趣的小伙伴可以留言讨论)

这里先针对上面这个图,给出 V8 工作的流程(后面解释):

  • 根据高级语言生成 AST 和作用域

  • 根据 AST 和作用域生成字节码

  • 解释执行字节码

  • 对热点代码(项目中反复使用的代码)进行监控

  • 编译热点代码为二进制的机器代码

  • 如果热点代码有所改变,需要进行反优化操作

V8 首先会接收到需要执行的 JavaScript 源码,但是这在 V8 看来只不过是一串字符串,V8 不能直接理解你写的 if else 的含义。

因此需要对这段字符串进行处理,转换。

V8 对源代码的进行处理、结构化之后,就生成了抽象语法树 (AST),AST 是为了 V8 能够理解源码的含义。

AST 分为两个阶段,先分词(词法分析),再解释(语法分析)。

AST 后面计划专门出一篇文章来讲,这里就不展开说了。又是后面,后面!!

有了 AST, V8 就可以生成该段代码的执行上下文。

执行上下文后面也会单独一篇来讲,执行上下文可以把作用域、作用域链,闭包、this 等知识点一起串起来讲。

见过不少面试的小伙伴,很多 JavaScript 基础都能说出来,但是不能比较完整的串起来。

有了 AST 和执行上下文,接下来轮到解释器小姐姐登场了,小姐姐根据 AST 生成字节码,然后解释执行字节码。

上面的 V8 流程图, 有一个 Hot 区域,其实这是一个监控解释器执行状态的模块,解释器在执行字节码的过程中,如果监控到某部分代码被多次重复执行,那么监模块会将这部分代码打上热点代码的标记。

当某部分代码被打上热点标之后,V8 就会将这部分字节码甩给优化编译器,优化编译器会在后台将这部分字节码编译为二进制。如果后面再执行到这部分代码时,V8 会优先选择编译之后的二进制,这样代码的执行速度就大大提升了。这就是即时编译(JIT)技术

然后,众所周知,JavaScript 是一门动态语言,运行时可以修改对象,但是经过优化编译器编译的代码只是针对某一种固定的结构,一旦对象的结构被动态修改,那么这部分编译优化的代码就需要反优化操作,否则就是无效代码。经过反优化的代码,下次执行时就会回退到解释器解释执行。

那除了 V8,还有哪些地方也用了 JIT 技术呢??

暖男怪怪给你小结了一下,还有这些:

  • 著名的JVM以及luajit

  • Oracle 的 GraaIVM

  • 苹果 SquirrelFish Extreme

  • Mozilla 的 SpiderMonkey

  • 等等(你还知道哪些,留言见)

上周跟同事讨论的时候,被问到一个问题说,V8执行的越久,被编译成机器码的热点代码就越多,整体执行效率也就越高。如果是这样,那 V8 内存占用同时也会越来越高,V8 是如何考虑这点的?(仿佛被同事面试了一把,噗呲~)

简单回答一下,V8 引入字节码,也就有了一个相对弹性的空间,内存和执行速度之间就可以去做调节。相比直接将 JS 代码全部编译成字节码(早期的 V8 其实就是这样,后来由于移动端兴起导致的内存问题,便有了现在的结构),这种模式就没有任何弹性的空间了!

总结

分享 V8 的执行流程,是怪怪觉得编译原理相关的东西对于我们来讲蛮重要的,会让你明白一些前端应用的本质。

也希望小伙伴们重视计算机基础,基于理论去实战比盲目的实战收获会更大,很多东西并不是记住就ok,还是要理解到本质,才是真的属于你自己的东西了。

觉得怪怪还不错的小伙伴,点击阅读原文,给怪怪掘金点个赞哦~

联系我 / 公众号