走进chrome内心,了解V8引擎是如何工作的丨技术开发分享录

走进chrome内心,了解V8引擎是如何工作的

{{ detail.nickname }}

转载 翻译 {{ formatTime(detail.create_time) }} 字数 {{ detail.content && detail.content.length }} 阅读 {{ detail.read_num }} {{ formatTag(v) }}

"## V8是什么\n\n在深入了解一件事物之前,首先要知道它是什么。\n\nV8是一个由Google开源的采用C++编写的高性能JavaScript和WebAssembly引擎,应用在 Chrome和Node.js等中。它实现了ECMAScript和WebAssembly,运行在Windows 7及以上、macOS 10.12+以及使用x64、IA-32、ARM或MIPS处理器的Linux系统上。 V8可以独立运行,也可以嵌入到任何C++应用程序中。\n\n## V8由来\n\n接下来我们来关心关心它如何诞生的,以及为什么叫这个名字。\n\nV8最初是由Lars Bak团队开发的,以汽车的V8发动机(有八个气缸的V型发动机)进行命名,预示着这将是一款性能极高的JavaScript引擎,在2008年9月2号同chrome一同开源发布。\n\n## 为什么需要V8\n\n我们写的JavaScript代码最终是要在机器中被执行的,但机器无法直接识别这些高级语言。需要经过一系列的处理,将高级语言转换成机器可以识别的的指令,也就是二进制码,交给机器执行。这中间的转换过程就是V8的具体工作。\n\n接下来我们就来详细的了解一下。\n\n## V8组成\n\n首先来看一下V8的内部组成。V8的内部有很多模块,其中最重要的4个如下:\n\n- Parser:  解析器,负责将源代码解析成AST\n- Ignition: 解释器,负责将AST转换成字节码并执行,同时会标记热点代码\n- TurboFan: 编译器,负责将热点代码编译成机器码并执行\n- Orinoco: 垃圾回收器,负责进行内存空间回收\n\n## V8工作流程\n\n以下是V8中几个重要模块的具体工作流程图。我们逐个分析。\n\n![](https://segmentfault.com/img/remote/1460000040331442)\n\n### Parser解析器\n\nParser解析器负责将源代码转换成抽象语法树AST。在转换过程中有两个重要的阶段:词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。\n\n词法分析\n也称为分词,是将字符串形式的代码转换为标记(token)序列的过程。这里的token是一个字符串,是构成源代码的最小单位,类似于英语中单词。词法分析也可以理解成将英文字母组合成单词的过程。词法分析过程中不会关心单词之间的关系。比如:词法分析过程中能够将括号标记成token,但并不会校验括号是否匹配。\n\nJavaScript中的token主要包含以下几种:\n\n- 关键字:`var、let、const`等\n- 标识符:没有被引号括起来的连续字符,可能是一个变量,也可能是 `if、else` 这些关键字,又或者是 `true、false` 这些内置常量\n- 运算符: `+、-、 *、/` 等\n- 数字:像十六进制,十进制,八进制以及科学表达式等\n- 字符串:变量的值等\n- 空格:连续的空格,换行,缩进等\n- 注释:行注释或块注释都是一个不可拆分的最小语法单元\n- 标点:大括号、小括号、分号、冒号等\n\n以下是`const a = 'hello world'`经过esprima词法分析后生成的tokens。\n\n```\n[\n    {\n        \"type\": \"Keyword\",\n        \"value\": \"const\"\n    },\n    {\n        \"type\": \"Identifier\",\n        \"value\": \"a\"\n    },\n    {\n        \"type\": \"Punctuator\",\n        \"value\": \"=\"\n    },\n    {\n        \"type\": \"String\",\n        \"value\": \"'hello world'\"\n    }\n]\n```\n\n### 语法分析\n\n语法分心是将词法分析产生的token按照某种给定的形式文法转换成AST的过程。也就是把单词组合成句子的过程。在转换过程中会验证语法,语法如果有错的话,会抛出语法错误。\n\n上述`const a = 'hello world'`经过语法分析后生成的AST如下:\n\n```\n{\n  \"type\": \"Program\",\n  \"body\": [\n    {\n      \"type\": \"VariableDeclaration\",\n      \"declarations\": [\n        {\n          \"type\": \"VariableDeclarator\",\n          \"id\": {\n            \"type\": \"Identifier\",\n            \"name\": \"a\"\n          },\n          \"init\": {\n            \"type\": \"Literal\",\n            \"value\": \"hello world\",\n            \"raw\": \"'hello world'\"\n          }\n        }\n      ],\n      \"kind\": \"const\"\n    }\n  ],\n  \"sourceType\": \"script\"\n}\n```\n\n经过Parser解析器生成的AST将交由Ignition解释器进行处理。\n\n### Ignition解释器\n\nIgnition解释器负责将AST转换成字节码(Bytecode)并执行。字节码是介于AST和机器码之间的一种代码,与特定类型的机器代码无关,需要通过解释器转换成机器码才可以执行。\n\n看到这里想必大家都有疑惑,既然字节码也需要转换成机器码才能运行,那一开始为什么不直接将AST转换成机器码直接运行呢?转换成机器码直接运行速度肯定更快,那为什么还要加一个中间过程呢?\n\n其实在V8的5.9版本之前是没有字节码的,而是直接将JS代码编译成机器码并将机器码存储到内存中,这样就占用了大量的内存,而早期的手机内存都不高,过度的占用会导致手机性能大大的下降;而且直接编译成机器码导致编译时间长,启动速度慢;再者直接将JS代码转换成机器码需要针对不同的CPU架构编写不同的指令集,复杂度很高。\n\n5.9版本以后引入了字节码,可以解决上述内存占用大、启动时间长、代码复杂度高这几个问题。\n\n接下来我们来看看Ignition是如何将AST转换成字节码的。\n\n下图是Ignition解释器的工作流程图。AST需要先通过字节码生成器,再经过一系列的优化之后才能生成字节码。\n\n![](https://segmentfault.com/img/remote/1460000040331443)\n\n其中的优化包括:\n\n- Register Optimizer:主要是避免寄存器不必要的加载和存储\n- Peephole Optimizer:寻找字节码中可以复用的部分,并进行合并\n- Dead-code Elimination: 删除无用的代码,减少字节码的大小\n\n将代码转换成字节码后就可以通过解释器执行了。Ignition在执行的过程中,会监视代码的执行情况并记录执行信息,如函数的执行次数、每次执行函数时所传的参数等。\n\n当同一段代码被执行多次,就会被标记成热点代码。热点代码会交给TurboFan编译器进行处理。\n\n### TurboFan编译器\n\nTurboFan拿到Ignition标记的热点代码后,会先进行优化处理,然后将优化后字节码编译成更高效的机器码存储起来。下次再次执行相同代码时,会直接执行相应的机器码,这样就在很大程度上提升了代码的执行效率。\n\n当一段代码不再是热点代码后,TurboFan会进行去优化的过程,将优化编译后的机器码还原成字节码,将代码的执行权利交还给Ignition。\n\n现在我们来看一看具体的执行过程。\n\n以`sum += arr[i]`为例,由于JS是动态类型的语言,每次的`sum`和`arr[i]`都有可能是不同的类型,在执行这段代码时,Ignition每次都会检查sum和arr[i]的数据类型。当发现同样的代码被执行了多次时,就将其标记为热点代码,交给TurboFan。\n\nTurboFan在执行时,如果每次都判断sum和arr[i]的数据类型是很浪费时间的。因此在优化时,会根据之前的几次执行确定sum和arr[i]的数据类型,将其编译成机器码。下次再执行时,省去了判断数据类型的过程。\n\n但如果在后续的执行过程中,arr[i]的数据类型发生了改变,之前生成的机器码就不满足要求了,TurboFan会把之前生成的机器码丢弃,将执行权利再交给Ignition,完成去优化的过程。\n\n热点代码:\n\n![](https://segmentfault.com/img/remote/1460000040331444)\n\n优化前:\n\n![](https://segmentfault.com/img/remote/1460000040331445)\n\n优化后:\n\n![](https://segmentfault.com/img/remote/1460000040331446)\n\n## 总结\n\n现在我们来总结一下V8的执行过程:\n\n源代码经过Parser解析器,经过词法分析和语法分析生成AST\nAST经过Ignition解释器生成字节码并执行\n在执行过程中,如果发现热点代码,将热点代码交给TurboFan编译器生成机器码并执行\n如果热点代码不再满足要求,进行去优化处理\n这种字节码与解释器和编译器结合的技术,就是我们通常所说的即时编译(JIT)。"
PS:写作不易,如要转裁,请标明转载出处。

如果此篇对您有帮助,可小额赞助,以兹鼓励!

猜你想看