Nodejs V8虚拟机

46 篇文章 0 订阅
2 篇文章 0 订阅

ㅤㅤㅤ
ㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ(人的理想志向往往和他的能力成正比。——约翰逊)
ㅤㅤㅤ
ㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ在这里插入图片描述

V8 blog

V8引擎是2008年发布的,它的命名灵感来自超级性能车的V8引擎

在这里插入图片描述

V8虚拟机的简介

V8 作为一个 JavaScript 引擎,最初是服役于 Google Chrome 浏览器的。它随着 Chrome 的第一版发布而发布以及开源。现在它除了 Chrome 浏览器,已经有很多其他的使用者了。诸如 NodeJS、MongoDB、CouchDB 等。
JavaScript 作为 Prototype-Based Language , 基于它使用 Prototype 继承的特征,V8 使用了直译的方式,即把 JavaScript 代码直接编译成机器码( Machine Code, 有些地方也叫 Native Code ),然后直接交由硬件执行。
与传统的「编译-解析-执行」的流程不同,V8 处理 JavaScript,并没有二进制码或其他的中间码。
简单来说,V8主要工作就是:「把 JavaScript 直译成机器码,然后运行」
但在8.5.9的版本中引入了Ignition,文章后续会讲到

V8虚拟机的历史

在这里插入图片描述

Google的Chrome中的V8 JavaScript引擎,由于性能良好吸引了相当的注目。它是Google特别为了Chrome可以高速运行网页应用(WebApp)而开发的。Chrome利用苹果领导的WebKit研发计划作为渲染引擎(Rendering engine)。 WebKit也被用在Safari浏览器中。WebKit的标准配备有称为JavaScriptCore的JavaScript引擎,但Chrome则以V8取代之
随着Web相关技术的发展,JavaScript所要承担的工作也越来越多,早就超越了“表单验证”的范畴,这就更需要快速的解析和执行JavaScript脚本。V8引擎就是为解决这一问题而生,在node中也是采用该引擎来解析JavaScript
V8开发小组是一群程序语言专家。核心工程师Lars Bak之前研发了HotSpot VM,这是用在Sun Microsystems公司开发的Java虚拟机器(VM)之加速技术。他也在美国的Animorphic Systems公司(于1997年被Sun Microsystems所并购)研发了称为Strongtalk的实验Smalltalk系统。V8充分发挥了研发HotSpot和Strongtalk时所获得的知识
在为数不多JavaScript引擎中,V8无疑是最流行的,Chrome与Node.js都使用了V8引擎,Chrome的市场占有率高达60%,而Node.js是JS后端编程的事实标准。国内的众多浏览器,其实都是基于Chromium浏览器开发,而Chromium相当于开源版本的Chrome,自然也是基于V8引擎的。神奇的是,就连浏览器界的独树一帜的Microsoft也投靠了Chromium阵营。另外,Electron是基于Node.js与Chromium开发桌面应用,也是基于V8的

V8和Nodejs的故事

Node.js由Ryan Dahl在2009年进行研发,它的发展和维护得到Joyent公司资助。Dahl在看到Flickr的上传文件进度条时萌发了研发Node.js的想法。由于浏览器不知道已经上传了多少文件,所以不得不向服务器发起请求查询。Dahl想到了一个更简单的方法。Ruby的Mongrel web server是Dahl的一个灵感来源。
Node.js受到其他一些模式的影响,例如Ruby On Rails的Event Machine,Python的Twisted。在这些模式基础之上,Node.js提供的事件循环(event loop)使之不再像先前那些模式一样只是一个类库,而成为一种语言。与传统使用阻塞调用的模式不同,Node.js没有循环事件所使用的调用,而是本身执行完脚本直接进入循环。这也是javascript的运行方式。
最初Dahl经历了几个失败的项目,这几个项目分别由C、Lua和Haskell写成的,但当谷歌发布V8引擎后,Dahl开始尝试Javascript。
尽管他最初的想法是非阻塞,但是他在模块系统和一些其他地方并没有遵循非阻塞,因为非阻塞导致加载外部类库时有问题。
Node.js由Dahl在2011年发布,但只能在Linux运行。npm作为Node.js的包管理工具在同年发布。
在2011年6月,微软与Joyent合作帮助开发了原生的Windows版本的Node.js。同年7月,第一个Windows版本Node.js发布。
2012年1月30日,Dahl将守护者位置让给他的同事,也是npm创始人Issac Schlueter。Dahl在Google groups中写道:
  “现在,基于libuv的重写工作已大部分完成,我将把我的守护者位置让与Issac Schlueter。在接下来的数月里,我们的精力将转移到关于第三方模块系统体验(其中包括一个查阅所有第三方模块的网站),一个新的插件构建系统,在npm增加二进制安装包。Issac将担任维系内核与外部模块关系并使之具有良好体验的角色,也是唯一具有这种维系能力的角色。经过三年对Node的开发,这使我可以空出来做一些项目研究工作。我仍然为Joyent工作,并从旁提供建议,但我不再进行日复一日的Bug修复工作。Issac具有让Node发布新版本的话语权。他将直接负责新特性诉求、更改和bug修复。”
2014年1月15日,Schlueter宣布他将把npm作为他的工作重心,Timothy J Fontaine将成为项目负责人。Issac在Node.js博客中写道:
  “经过去年一年,TJ Fontaine已经变成Node.js项目的绝对核心。他一直从事构建新版本,管理测试工作,修复烦人的bug,始终关注使用者的需求并作出最后决定。……任何接触到项目的核心工作的人都知道他已经作为事实上负责项目有一段时间了,因此我们决定将它变成正式的。这个决定即时生效,TJ Fontaine成为Node.js项目负责人。我将仍是Node.js的核心开发者,并期待继续以这个角色为项目作出自己的贡献。无论如何,我工作重心将是npm。”
第二天,2014年1月16日,Timothy J Fontaine回帖简要说明了以后的发展道路,还有bug修复,性能平衡,与V8引擎保持同步更新以及工具。

V8引擎的内部结构

V8由许多子模块构成,其中这4个模块是最重要的:

  • Parser:负责将JavaScript源码转换为Abstract Syntax Tree (AST)语法树
  • Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集- – TurboFan优化编译所需的信息,比如函数参数的类型;
  • TurboFan:compiler,即编译器,利用Ignitio所收集的类型信息,将Bytecode转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收;

简单地说,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

在这里插入图片描述

Abstract Syntax Tree (AST)

语法树生成链接

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式
 
在这里插入图片描述

type //描述该语句的类型 --变量声明语句
kind //变量声明的关键字 -- var
declaration://声明的内容数组,里面的每一项也是一个对象
	type //描述该语句的类型 
	id //描述变量名称的对象
		type //定义
		name //是变量的名字
    init //初始化变量值得对象
		type //类型
		value //值 "is tree" 不带引号
		row //"\"is tree"\" 带引号

为什么需要AST?

  • 代码语法的检查,代码风格的检查,代码的格式化,代码的高亮,代码错误提示,代码自动补全
       
    如:JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
    IDE的错误提示,格式化,高亮,自动补全等等
    代码的混淆压缩

  • 优化变更代码,改变代码结构达到想要的结构
     
    代码打包工具webpack,rollup等等
    CommonJS、AMD、CMD、UMD等代码规范之间的转化
    CoffeeScript、TypeScript、JSX等转化为原生Javascript

Ignition

Ignition(解释器) 项目的目标是为 V8 建立一个解释器来执行低层级的字节码,以便让那些只被运行一次或者非热点的代码以字节码的形式更紧凑地存储。由于字节码更小,编译时间也将大幅减少,我们也能在初始编译时更激进,以此来明显地改善启动时间。还有一个额外的优势:字节码可以被直接丢给 TurboFan 图生成器,从而在 TurboFan 里面优化函数时,可以避免重新解析 JavaScript 源代码
   
它本身由一系列字节码处理程序代码片段组成,每个片段都处理一个特定的字节码,然后调度给下一个字节码的处理程序。这些字节码处理程序使用高层级的、机器架构无关的汇编代码写成,由 CodeStubAssembler(V8的一个组件,它允许我们直接用C ++编写低级别的TurboFan IR(intermediate representation),TurboFan的后端(编译器后端)则将其翻译为相应架构上的机器码)类实现,并由 TurboFan 编译
   
v8在v8 5.9版本后,默认启动了Ignition(字节码解释器),在这之前,是直接使用full-codegen编译器生成机器码的
之所以又采用了Ignition字节码解释器,主要是因为,没有经过优化的代码导致v8在性能上没有较大的突破,且full-codegen 编译器生成的机器码较为冗余。
 
启用ignition的好处

  • 将代码空间大小减小至当前的 50% 左右
  • 与 full-codegen 相比有合理的性能
  • 完整地支持 DevTools 调试和 CPU 性能分析
  • 替代 full-codegen 成为第一层编译器
  • 作为 TurboFan 编译器的一个新的前端,要能在重新优化编译时,不必重新解析 JS 源代码
  • 支持从 TurboFan 解优化到 Ignition 解释器

V8回归Ignition的具体信息

Deoptimization

主流的JavaScript引擎为了优化JavaScript程序的性能,都采用JIT (Just-In-Time)编译技术在运行时对各个程序点中变量的类型集合进行假设,并在此基础上对程序进行类型特化.但是JavaScript的动态类型和基于原型的语言特性使得JavaScript程序的类型可预测性不高,这导致引擎所做出的假设经常失败,频繁引发Deoptimization(优化回滚),带来额外的开销.
 
这是因为Ignition收集的信息可能是错误的,比如add函数的参数之前是整数,后来又变成了字符串。生成的Optimized Machine Code已经假定add函数的参数是整数,那当然是错误的,于是需要进行Deoptimization

优化回滚

早期的V8是基于AST直接生成本地代码,省去了字节码部分,所以本地代码尚未经过很好的优化。于是,在2010年,V8引入了新的编译器-Crankshaft(曲轴),它主要针对热点函数进行优化,基于JavaScript源代码开始分析而非本地代码,同时构建Hydrogen(氢)图并基于此来进行优化分析

const counter = 0;
function test(x, y) {
    counter++;
    if (counter < 1000000) {
        // do something
        return 'zzw';
    }
    const unknown = new Date();
    console.log(unknown);
}

函数被调用多次之后,V8引擎可能会触发Crankshaft编译器对其进行优化,而优化代码认为示例代码的类型信息都已经被确定。但,由于尚未真正执行到new Date()这个地方,并未获取unknown这个变量的类型,V8只得将该部分代码进行回滚。优化回滚是一个很耗时的操作,在写代码过程中,尽量不要触发优化该操作

隐藏类

在JavaScript中获取变量信息需要通过字符串匹配的方式查找属性值,这就需要更多的操作才能访问到变量信息,而代码量变量存取是十分频繁的,这也就制约了JavaScript的性能。V8借用了类和偏移位置的思想,将本来通过属性名匹配来访问属性值的方法进行了改进,使用类似C++编译器的偏移位置机制来实现,这就是隐藏类
 
隐藏类将对象划分成不同的组,对于组内对象拥有相同的属性名和属性值的情况,将这些组的属性名和对应的偏移位置保存在一个隐藏类中,组内所有对象共享该信息。同时,也可以识别属性不同的对象。示例如下

在这里插入图片描述

使用Person构造了两个对象fun1和fun2,这两个对象具有相同的属性名和参数类型,V8将它们归为同一个组,也就是隐藏类,这些属性在隐藏类中有相同的偏移值,fun1和fun2共享这一信息,进行属性访问时,只需根据隐藏类的偏移值即可。由于JavaScript是动态类型语言,在执行时可以更改变量的类型,如果上述代码执行之后,执行fun1.address=‘纽约’,那么fun1和fun2将不再被认为是一个组,V8将对代码进行再优化,并将fun1变更为新的隐藏类

内联缓存

正常访问对象属性的过程是:首先获取隐藏类的地址,然后根据属性名查找偏移值,然后计算该属性的地址。虽然相比以往在整个执行环境中查找减小了很大的工作量,但依然比较耗时。能不能将之前查询的结果缓存起来,供再次访问呢?当然是可行的,这就是内嵌缓存。
 
内嵌缓存的大致思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否是之前的隐藏类,如果是的话,直接使用之前的缓存结果,减少再次查找表的时间。当然,如果一个对象有多个属性,那么缓存失误的概率就会提高,因为某个属性的类型变化之后,对象的隐藏类也会变化,就与之前的缓存不一致,需要重新使用以前的方式查找哈希表

V8的内存管理

V8垃圾回收机制
V8垃圾回收日志分析

V8快照

延迟反序列化(Lazy deserialization)功能在 V8 6.4 中默认打开了,带来的好处就是平均每个 Chrome 的选项卡可以节约500KB左右的内存消耗。

在V8引擎启动时,需要构建JavaScript运行环境,需要加载很多内置对象,同时也需要建立内置的函数,如Array,String,Math等。
在这里插入图片描述

为了使V8更加整洁,加载对象和建立函数等任务都是使用JavaScript文件来实现的,V8引擎负责提供机制来支持,就是在编译和执行JavaScript前先加载这些文件。

  • JavaScript 规范包含许多内置功能,例如从数学函数到全功能的正则表达式引擎。每个新创建的 V8 上下文(V8 context)从开始就可以使用这些功能。为此,必须在创建上下文时在 V8 的堆上初始化这些功能,将全局对象(例如,浏览器中的窗口对象)和所有内置功能初始化。如果这一切都是从头开始,那么需要相当长的一段时间
  • 因此,为了解决这项操作频繁而又可重复的工作,V8提出一种快照机制,即将context初始化后的堆内存(已经加载完成JS的内置库的运行环境)序列化并存储到本地磁盘作为快照文件,在下次需要初始化context时直接将快照中的数据反序列化到堆中以达到初始化context的效果。这样用反序列化快照文件来替代对内置JS库的加载,大幅提升了初始化context的效率。在一台普通的桌面PC上,这个方法可以将创建上下文的时间从 40ms 缩减 2ms;在一台普通的移动设备上,这个是数字是从 270ms 到 10ms

Just-in-time compilation

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

JIT(just-in-time compilation)是动态编译的一种形式,是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译与动态直译。静态编译的程序在执行前全部被翻译为机器码,而直译执行的则是一句一句边运行边翻译。即时编译器则混合了这二者,以文件、函数、代码块为代码进行按需编译,并将翻译过的代码进行优化并缓存起来,在接下来的执行中,无需编译便可执行

V8早期的JIT编译器是full-codegen,后来又实现了Crankshaft,直到现在的TurboFan

为什么需要JIT

JIT在编译时可以利用当前运行期的状态下上文来指导编译优化。这种优势使得JIT在性能上超过静态编译成为可能(如下图所示,JIT 编译器的初始阶段性能很低,因为要首先解释方法。随着编译方法的增多及 JIT 执行编译所需时间的缩短,性能曲线逐渐升高最后达到性能峰值。另一方面,AOT(程序运行前编译) 编译代码启动时的性能比解释的性能高很多,但是无法达到 JIT 编译器所能达到的最高性能。),虽然现实并非如此
在这里插入图片描述

  • JIT优点

通过在运行时收集监控信息,把"热点代码"(Hot Spot Code)编译成与本地平台相关的机器码,并进行各种层次的优化,大大提高效率

  • JIT缺点

收集监控信息影响程序运行。编译过程占用程序运行时间(如使得启动速度变慢)。编译机器码占用内存

  • JIT带来的问题

JIT需要在第一运行时进行编译,因此势必会增加执行的时间,并且编译的时间一般都很长,可能会比执行时间长很多,如果编译后,该方法只运行一次,极有可能会导致得不偿失,如果编译后,该方法反复运行上万次,那么收益应该比较明显。对于这个问题业界称之为"startup delay"JIT需要在第一运行时进行编译,因此势必会增加执行的时间,并且编译的时间一般都很长,可能会比执行时间长很多,如果编译后,该方法只运行一次,极有可能会导致得不偿失,如果编译后,该方法反复运行上万次,那么收益应该比较明显。对于这个问题业界称之为"startup delay(启动延迟)"
   
为了克服这个缺点,现代的 JIT 编译器使用了下面两种方法的任意一种(某些情况下同时使用了这两种方法)。第一种方法是:编译所有的代码,但是不执行任何耗时多的分析和转换,因此可以快速生成代码。由于生成代码的速度很快,因此尽管可以明显观察到编译带来的开销,但是这很容易就被反复执行本地代码所带来的性能改善所掩盖。第二种方法是:将编译资源只分配给少量的频繁执行的方法(通常称作热方法)。低编译开销更容易被反复执行热代码带来的性能优势掩盖。很多应用程序只执行少量的热方法,因此这种方法有效地实现了编译性能成本的最小化。
   
动态编译器的一个主要的复杂性在于权衡了解编译代码的预期获益使方法的执行对整个程序的性能起多大作用。一个极端的例子是,程序执行后,您非常清楚哪些方法对于这个特定的执行的性能贡献最大,但是编译这些方法毫无用处,因为程序已经完成。而在另一个极端,程序执行前无法得知哪些方法重要,但是每种方法的潜在受益都最大化了。大多数动态编译器的操作介于这两个极端之间,方法是权衡了解方法预期获益的重要程度

总结

相信读者在阅读完之后,对V8虚拟机也有了一些基本的认识。虽然V8虚拟机的性能很强,但毕竟JavaScript语言是一个弱类型语言,只能在运行时才能确定类型,频繁的对数据类型进行更改必然会影响语言性能。下面总结一些在日常开发中的一些小技巧,更好的利用V8的代码优化机制

  • 尽量先定义好JSON的所有属性和类型,避免对JSON属性key进行新增,删除和更改,以及更改值的数据类型,因为这样会使V8重组该隐藏类的数据结构,并且会触发再优化
  • 避免多次调用只执行一次的函数,避免V8虚拟机误认为是热点函数,对它进行再优化
  • 避免频繁更改变量和函数的参数类型,因为V8虚拟机会因为数据类型的更改而频繁进行再优化
  • 使用TypeSript,虽然是JS的超集,但如果有良好的团队代码规范(避免写成anyScript)和适当的CodeReview(代码审查),不仅能在一定程度上提高代码的类型安全和执行性能,也能提高服务整体的可维护和可读性
参考文章
  • http://blog.sina.com.cn/s/blog_742eb90201015a2g.html    JIT
  • https://zhuanlan.zhihu.com/p/103904567              V8引擎
  • https://zhuanlan.zhihu.com/p/27628685              认识V8引擎
  • https://developer.aliyun.com/article/580951            V8快照-延迟反序列化
  • https://zhuanlan.zhihu.com/p/32249462               从Context创建流程学习V8快照机制
  • https://hllvm-group.iteye.com/group/topic/37596      各JavaScript虚拟机相关资料
  • https://www.cnblogs.com/qinmengjiao123-123/p/8648488.html    AST
  • https://zhuanlan.zhihu.com/p/41496446           Ignition
  • https://zhuanlan.zhihu.com/p/26669846 JS 引擎与字节码的不解之缘
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值