JavaScript绝对是最火的编程语言之一,一直具备很大的用户群,随着在服务端的使用(NodeJs),更是爆发了极强的生命力。编程语言分为编译型语言和解释型语言两类,编译型语言在执行以前要先进行彻底编译,而解释型语言一边编译一边执行,很明显解释型语言的执行速度是慢于编译型语言的,而JavaScript就是一种解释型脚本语言,支持动态类型、弱类型、基于原型的语言,内置支持类型。鉴于JavaScript都是在前端执行,并且须要及时响应用户,这就要求JavaScript能够快速的解析及执行。css
随着Web相关技术的发展,JavaScript所要承担的工做也愈来愈多,早就超越了“表单验证”的范畴,这就更须要快速的解析和执行JavaScript脚本。V8引擎就是为解决这一问题而生,在node中也是采用该引擎来解析JavaScript。前端
V8是如何使得JavaScript性能有大幅提高的呢?经过对一些书籍和文章的学习,梳理了V8的相关内容,本文将带你认识 V8。(该文在 17 年初发布于公司内网,反响不错,近来闲暇再次整理做为知乎的第一篇分享,但愿帮助更多的人了解 V8 引擎。转载需通过本人赞成)java
浏览器自从上世纪80年代后期90年代初期诞生以来,已经获得了长足的发展,其功能也愈来愈丰富,包括网络、资源管理、网页浏览、多页面管理、插件和扩展、书签管理、历史记录管理、设置管理、下载管理、帐户和同步、安全机制、隐私管理、外观主题、开发者工具等。在这些功能中,为用户提供网页浏览服务无疑是最重要的功能,下面将对相关内容进行介绍。node
渲染引擎:可以将HTML/CSS/JavaScript文本及相应的资源文件转换成图像结果。渲染引擎的主要做用是将资源文件转化为用户可见的结果。在浏览器的发展过程当中,不一样的厂商开发了不一样的渲染引擎,如Tridend(IE)、Gecko(FF)、WebKit(Safari,Chrome,Andriod浏览器)等。WebKit是由苹果2005年发起的一个开源项目,引发了众多公司的重视,几年间被不少公司所采用,在移动端更占据了垄断地位。更有甚者,开发出了基于WebKit的支持HTML5的web操做系统(如:Chrome OS、Web OS)。linux
下面是WebKit的大体结构:android
上图中实线框内模块是全部移植的共有部分,虚线框内不一样的厂商能够本身实现。下面进行介绍:c++
上面介绍了渲染引擎的各个模块,那么一张网页,要经历怎样的过程,才能抵达用户面前?web
首先是网页内容,输入到HTML解析器,HTML解析器解析,而后构建DOM树,在这期间若是遇到JavaScript代码则交给JavaScript引擎处理;若是来自CSS解析器的样式信息,构建一个内部绘图模型。该模型由布局模块计算模型内部各个元素的位置和大小信息,最后由绘图模块完成从该模型到图像的绘制。在网页渲染的过程当中,大体可分为下面3个阶段。正则表达式
上述是一个完整的渲染过程,现代网页不少都是动态的,随着网页与用户的交互,浏览器须要不断的重复渲染过程。算法
JavaScript本质上是一种解释型语言,与编译型语言不一样的是它须要一遍执行一边解析,而编译型语言在执行时已经完成编译,可直接执行,有更快的执行速度(如上图所示)。JavaScript代码是在浏览器端解析和执行的,若是须要时间太长,会影响用户体验。那么提升JavaScript的解析速度就是当务之急。JavaScript引擎和渲染引擎的关系以下图所示:
JavaScript语言是解释型语言,为了提升性能,引入了Java虚拟机和C++编译器中的众多技术。如今JavaScript引擎的执行过程大体是:
源代码-→抽象语法树-→字节码-→JIT-→本地代码(V8引擎没有中间字节码)。一段代码的抽象语法树示例以下:
function demo(name) { console.log(name); }
抽象语法树以下:
V8更加直接的将抽象语法树经过JIT技术转换成本地代码,放弃了在字节码阶段能够进行的一些性能优化,但保证了执行速度。在V8生成本地代码后,也会经过Profiler采集一些信息,来优化本地代码。虽然,少了生成字节码这一阶段的性能优化,但极大减小了转换时间。
可是在2017年4月底,v8 的 5.9 版本发布了,新增了一个 Ignition 字节码解释器,将默认启动,今后以后将与JSCore有大体相同的流程。作出这一改变的缘由为:(主要动机)减轻机器码占用的内存空间,即牺牲时间换空间;提升代码的启动速度;对 v8 的代码进行重构,下降 v8 的代码复杂度(V8 Ignition:JS 引擎与字节码的不解之缘 - CNode技术社区)。
JavaScript的性能和C相比还有不小的距离,可预见的将来估计也只能接近它,而不是与它相比,这从语言类型上已经决定。下面将对V8引擎进行更为细致的介绍。
V8引擎是一个JavaScript引擎实现,最初由一些语言方面专家设计,后被谷歌收购,随后谷歌对其进行了开源。V8使用C++开发,,在运行JavaScript以前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),而且使用了如内联缓存(inline caching)等方法来提升性能。有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序。V8支持众多操做系统,如windows、linux、android等,也支持其余硬件架构,如IA32,X64,ARM等,具备很好的可移植和跨平台特性。
V8项目代码结构以下:
JavaScript是一种无类型语言,在编译时并不能准确知道变量的类型,只能够在运行时肯定,这就不像c++或者java等静态类型语言,在编译时候就能够确切知道变量的类型。然而,在运行时计算和决定类型,会严重影响语言性能,这也就是JavaScript运行效率比C++或者JAVA低不少的缘由之一。
在C++中,源代码须要通过编译才能执行,在生成本地代码的过程当中,变量的地址和类型已经肯定,运行本地代码时利用数组和位移就能够存取变量和方法的地址,不须要再进行额外的查找,几个机器指令便可完成,节省了肯定类型和地址的时间。因为JavaScript是无类型语言,那就不能像c++那样在执行时已经知道变量的类型和地址,须要临时肯定。JavaScript 和C++有如下几个区别:
在代码执行过程当中,变量的存取是很是广泛和频繁的,经过偏移量来存取,使用少数两个汇编指令就能完成,若是经过属性名匹配则须要更多的汇编指令,也须要更多的内存空间。示例以下:
在JavaScript中,除boolean,number,string,null,undefined这个五个简单变量外,其余的数据都是对象,V8使用一种特殊的方式来表示它们,进而优化JavaScript的内部表示问题。
在V8中,数据的内部表示由数据的实际内容和数据的句柄构成。数据的实际内容是变长的,类型也是不一样的;句柄固定大小,包含指向数据的指针。这种设计能够方便V8进行垃圾回收和移动数据内容,若是直接使用指针的话就会出问题或者须要更大的开销,使用句柄的话,只需修改句柄中的指针便可,使用者使用的仍是句柄,指针改动是对使用者透明的。
除少数数据(如整型数据)由handle自己存储外,其余内容限于句柄大小和变长等缘由,都存储在堆中。整数直接从value中取值,而后使用一个指针指向它,能够减小内存的占用并提升访问速度。一个句柄对象的大小是4字节(32位设备)或者8字节(64位设备),而在JavaScriptCore中,使用的8个字节表示句柄。在堆中存放的对象都是4字节对齐的,因此它们指针的后两位是不须要的,V8用这两位表示数据的类型,00为整数,01为其余。
JavaScript对象在V8中的实现包含三个部分:隐藏类指针,这是v8为JavaScript对象建立的隐藏类;属性值表指针,指向该对象包含的属性值;元素表指针,指向该对象包含的属性。
前面有过介绍,V8引擎在执行JavaScript的过程当中,主要有两个阶段:编译和运行,与C++的执行前彻底编译不一样的是,JavaScript须要在用户使用时完成编译和执行。在V8中,JavaScript相关代码并不是一下完成编译的,而是在某些代码须要执行时,才会进行编译,这就提升了响应时间,减小了时间开销。在V8引擎中,源代码先被解析器转变为抽象语法树(AST),而后使用JIT编译器的全代码生成器从AST直接生成本地可执行代码。这个过程不一样于JAVA先生成字节码或中间表示,减小了AST到字节码的转换时间,提升了代码的执行速度。但因为缺乏了转换为字节码这一中间过程,也就减小了优化代码的机会。
V8引擎编译本地代码时使用的主要类以下所示:
JavaScript代码编译的过程大体为:Script类调用Compiler类的Compile函数为其生成本地代码。Compile函数先使用Parser类生成AST,再使用FullCodeGenerator类来生成本地代码。本地代码与具体的硬件平台密切相关,FullCodeGenerator使用多个后端来生成与平台相匹配的本地汇编代码。因为FullCodeGenerator经过遍历AST来为每一个节点生成相应的汇编代码,缺失了全局视图,节点之间的优化也就无从谈起。
在执行编译以前,V8会构建众多全局对象并加载一些内置的库(如math库),来构建一个运行环境。并且在JavaScript源代码中,并不是全部的函数都被编译生成本地代码,而是延迟编译,在调用时才会编译。
因为V8缺乏了生成中间代码这一环节,缺乏了必要的优化,为了提高性能,V8会在生成本地代码后,使用数据分析器(profiler)采集一些信息,而后根据这些数据将本地代码进行优化,生成更高效的本地代码,这是一个逐步改进的过程。同时,当发现优化后代码的性能还不如未优化的代码,V8将退回原来的代码,也就是优化回滚。下面介绍一下运行阶段,该阶段使用的主要类以下所示:
先根据须要编译和生成这些本地代码,也就是使用编译阶段那些类和操做。在V8中,函数是一个基本单位,当某个JavaScript函数被调用时,V8会查找该函数是否已经生成本地代码,若是已经生成,则直接调用该函数。不然,V8引擎会生成属于该函数的本地代码。这就节约了时间,减小了处理那些使用不到的代码的时间。其次,执行编译后的代码为JavaScript构建JS对象,这须要Runtime类来辅组建立对象,并须要从Heap类分配内存。再次,借助Runtime类中的辅组函数来完成一些功能,如属性访问等。最后,将不用的空间进行标记清除和垃圾回收。
由于V8是基于AST直接生成本地代码,没有通过中间表示层的优化,因此本地代码还没有通过很好的优化。因而,在2010年,V8引入了新的编译器-Crankshaft,它主要针对热点函数进行优化,基于JavaScript源代码开始分析而非本地代码,同时构建Hydroger图并基于此来进行优化分析。
Crankshaft编译器为了性能考虑,一般会作出比较乐观和大胆的预测—代码稳定且变量类型不变,因此能够生成高效的本地代码。可是,鉴于JavaScript的一个弱类型的语言,变量类型也可能在执行的过程当中进行改变,鉴于这种状况,V8会将该编译器作的想固然的优化进行回滚,称为优化回滚。
示例以下:
var counter = 0; function test(x, y) { counter++; if (counter < 1000000) { // do something return 'jeri'; } var unknown = new Date(); console.log(unknown); }
该函数被调用屡次以后,V8引擎可能会触发Crankshaft编译器对其进行优化,而优化代码认为示例代码的类型信息都已经被肯定。但,因为还没有真正执行到new Date()这个地方,并未获取unknown这个变量的类型,V8只得将该部分代码进行回滚。优化回滚是一个很耗时的操做,在写代码过程当中,尽可能不要触发优化该操做。
在最近发布的 V8 5.9 版本中,新增了一个 Ignition 字节码解释器,TurboFan 和 Ignition 结合起来共同完成JavaScript的编译。这个版本中消除 Cranshaft 这个旧的编译器,并让新的 Turbofan 直接从字节码来优化代码,并当须要进行反优化的时候直接反优化到字节码,而不须要再考虑 JS 源代码。
在执行C++代码时,仅凭几个指令便可根据偏移信息获取变量信息,而JavaScript里须要经过字符串匹配来查找属性值的,这就须要更多的操做才能访问到变量信息,而代码量变量存取是十分频繁的,这也就制约了JavaScript的性能。V8借用了类和偏移位置的思想,将原本经过属性名匹配来访问属性值的方法进行了改进,使用相似C++编译器的偏移位置机制来实现,这就是隐藏类。
隐藏类将对象划分红不一样的组,对于组内对象拥有相同的属性名和属性值的状况,将这些组的属性名和对应的偏移位置保存在一个隐藏类中,组内全部对象共享该信息。同时,也能够识别属性不一样的对象。示例以下:
使用Point构造了两个对象p和q,这两个对象具备相同的属性名,V8将它们归为同一个组,也就是隐藏类,这些属性在隐藏类中有相同的偏移值,p和q共享这一信息,进行属性访问时,只需根据隐藏类的偏移值便可。因为JavaScript是动态类型语言,在执行时能够更改变量的类型,若是上述代码执行以后,执行q.z=2,那么p和q将再也不被认为是一个组,q将是一个新的隐藏类。
正常访问对象属性的过程是:首先获取隐藏类的地址,而后根据属性名查找偏移值,而后计算该属性的地址。虽然相比以往在整个执行环境中查找减少了很大的工做量,但依然比较耗时。能不能将以前查询的结果缓存起来,供再次访问呢?固然是可行的,这就是内嵌缓存。
内嵌缓存的大体思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是不是以前的隐藏类,若是是的话,直接使用以前的缓存结果,减小再次查找表的时间。固然,若是一个对象有多个属性,那么缓存失误的几率就会提升,由于某个属性的类型变化以后,对象的隐藏类也会变化,就与以前的缓存不一致,须要从新使用之前的方式查找哈希表。
Node中经过JavaScript使用内存时就会发现只能使用部份内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB),其深层缘由是 V8 垃圾回收机制的限制所致(若是可以使用内存太大,V8在进行垃圾回收时需耗费更多的资源和时间,严重影响JS的执行效率)。下面对内存管理进行介绍。
内存的管理组要由分配和回收两个部分构成。V8的内存划分以下:
年轻分代:为新建立的对象分配内存空间,常常须要进行垃圾回收。为方便年轻分代中的内容回收,可再将年轻分代分为两半,一半用来分配,另外一半在回收时负责将以前还须要保留的对象复制过来。
年老分代:根据须要将年老的对象、指针、代码等数据保存起来,较少地进行垃圾回收。
大对象:为那些须要使用较多内存对象分配内存,固然一样可能包含数据和代码等分配的内存,一个页面只分配一个对象。
V8 使用了分代和大数据的内存分配,在回收内存时使用精简整理的算法标记未引用的对象,而后消除没有标记的对象,最后整理和压缩那些还未保存的对象,便可完成垃圾回收。
在V8中,使用较多的是年轻分代和年老分代。年轻分代中的对象垃圾回收主要经过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了Cheney算法:经过复制的方式实现的垃圾回收算法。它将堆内存分为两个 semispace,一个处于使用中(From空间),另外一个处于闲置状态(To空间)。当分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。在垃圾回收的过程当中,就是经过将存活对象在两个 semispace 空间之间进行复制。年轻分代中的对象有机会晋升为年老分代,条件主要有两个:一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。
对于年老分代中的对象,因为存活对象占较大比重,再采用上面的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低;另外一个问题依然是浪费一半空间的问题。为此,V8在年老分代中主要采用了Mark-Sweep(标记清除)标记清除和Mark-Compact(标记整理)相结合的方式进行垃圾回收。
在V8引擎启动时,须要构建JavaScript运行环境,须要加载不少内置对象,同时也须要创建内置的函数,如Array,String,Math等。为了使V8更加整洁,加载对象和创建函数等任务都是使用JavaScript文件来实现的,V8引擎负责提供机制来支持,就是在编译和执行JavaScript前先加载这些文件。
V8引擎须要编译和执行这些内置的JavaScript代码,同时使用堆等来保存执行过程当中建立的对象、代码等,这些都须要时间。为此,V8引入了快照机制。将这些内置的对象和函数加载以后的内存保存并序列化。序列化以后的结果很容易反序列化,通过快照机制的启动时间能够缩减几毫秒。快照机制也能够将一些开发者认为须要的JavaScript文件序列化,以减小处理时间。不过快照机制的加载的代码不能被CrankShaft这样的编译器优化,可能会存在性能问题。
JavaScriptCore引擎是WebKit中默认的JavaScript引擎,也是苹果开源的一个项目,应用较为普遍。最初,性能不是很好,从2008年开始了一系列的优化,从新实现了编译器和字节码解释器,使得引擎的性能有较大的提高。随后内嵌缓存、基于正则表达式的JIT、简单的JIT及字节码解释器等技术引入进来,JavaScriptCore引擎也在不断的迭代和发展。
V8引擎自诞生之日起就以性能优化做为目标,引入了众多新技术,极大了带动了整个业界JavaScript引擎性能的快速发展。总的来讲,V8引擎较为激进,青睐能够提升性能的新技术,而JavaScriptCore引擎较为稳健,渐进式的改变着本身的性能。总的来讲JavaScript引擎工做流程(包含v8和JavaScriptCore)以下所示:
JavaScriptCore 的大体流程为:源代码-→抽象语法树-→字节码-→JIT-→本地代码。JavaScriptCore与V8有一些不一样之处,其中最大的不一样就是新增了字节码的中间表示,并加入了多层JIT编译器(如:简单JIT编译器、DFG JIT编译器、LLVM等)优化性能,不停的对本地代码进行优化。(在 V8 的 5.9 版本中,新增了一个 Ignition 字节码解释器,TurboFan 和 Ignition 结合起来共同完成JavaScript的编译,此后 V8 将与 JavaScriptCore 有大体相同的流程,Node 8.0中 V8 版本为 5.8)
还有就是在数据表示方面,V8在不一样的机器上使用与机器位数相匹配的数据表示,而在JavaScriptCore中句柄都是使用64位表示,其能够表示更大范围的数字,因此即便在32位机器上,浮点类型一样能够保存在句柄中,再也不须要访问堆中的数据,当也会占用更多的空间。
JavaScript引擎的主要功能是解析和执行JavaScript代码,每每不能知足使用者多样化的须要,那么就能够增长扩展以提高它的能力。V8引擎有两种扩展机制:绑定和扩展。
使用IDL文件或接口文件生成绑定文件,将这些文件同V8引擎一块儿编译。WebKit中使用IDL来定义JavaScript,但又与IDL有所不一样,有一些改变。定义一个新的接口的步骤大体以下:
module mymodule { interface [ InterfaceName = MyObject ] MyObj { readonly attribute long myAttr; DOMString myMethod (DOMString myArg); }; }
JavaScript引擎绑定机制须要将扩展代码和JavaScript引擎一块编译和打包,不能根据须要在引擎启动后再动态注入这些本地代码。在实际WEB开发中,开发者都是基于现有浏览器的,根本不可能介入到JavaScript引擎的编译中,绑定机制有很大的局限性,但其很是高效,适用于对性能要求较高的场景。
经过V8的基类Extension进行能力扩展,无需和V8引擎一块儿编译,能够动态为引擎增长功能特性,具备很大的灵活性。
Extension机制的大体思路就是,V8提供一个基类Extension和一个全局注册函数,要想扩展JavaScript能力,须要通过如下步骤:
class MYExtension : public v8::Extension { public: MYExtension() : v8::Extension("v8/My", "native function my();") {} virtual v8::Handle<v8::FunctionTemplate> GetNativeFunction ( v8::Handle<v8::String> name) { // 能够根据name来返回不一样的函数 return v8::FunctionTemplate::New(MYExtention::MY); } static v8::Handle<v8::Value> MY(const v8::Arguments& args) { // Do sth here return v8::Undefined(); } }; MYExtension extension; RegisterExtension(&extension);
Extension机制是调用V8的接口注入新函数,动态扩展很是方便,但没有绑定机制高效,适用于对性能要求不高的场景。
在过去几年,JavaScript在不少领域获得了普遍的应用,然而限于JavaScript语言自己的不足,执行效率不高。Google也推出了一些JavaScript网络应用,如Gmail、Google Maps及Google Docs office等。这些应用的性能不只受到服务器、网络、渲染引擎以及其余诸多因素的影响,同时也受到JavaScript自己执行速度的影响。然而既有的JavaScript引擎没法知足新的需求,而性能不佳一直是网络应用开发者最关心的。Google就开始了V8引擎的研究,将一系列新技术引入JavaScript引擎中,大大提升了JavaScript的执行效率。相信随着V8引擎的不断发展,JavaScript也会有更普遍的应用场景,前端工程师也会有更好的将来!
那么结合上面对于V8引擎的介绍,咱们在编程中应注意: