您的位置:首页 > Web前端 > JavaScript

JavaScript引擎

2016-06-07 00:01 387 查看
本节主要介绍JavaScriptCore引擎和V8引擎

概述

[b]JavaScript语言[/b]

JavaScript是一种脚本语言,主要用在Web的客户端,它的出现主要是控制网页客户端的逻辑,例如同用户的交互、异步通信等需求,本质上看它是一种解释型语言,函数是它的第一等公民,也就是函数也能够当作参数或返回值来传递。JavaScript是一种无类型语言,或者说是动态类型语言,而c++或Java等语言是静态类型语言,它们在编译的时候就能够知道每个变量的类型,但JavaScript只能在运行时候才能确定,而运行时计算和决定类型会带来严重的性能损失,这导致了JavaScript语言的运行效率比c++和Java低很多。

获取对象属性值的具体位置也称为相对于对象基地址的偏移位置,JavaScript和c++语言存在一下几个部分的区别:

编译确定位置:c++有明确的两个阶段,而编译这些位置的偏移信息都是编译器在编译的时候就决定下来的,当c++代码编程成本地代码之后,对象的属性和偏移信息都计算完成,因为JavaScript没有类型,所以只有在对象创建的时候才有这些信息,因而只能在执行阶段确定,而且JavaScript语言能够在执行时修改对象的属性

偏移信息共享:c++因为有类型定义,所有所有对象都是按照该类型来确定的,而且不能在执行的时候动态改变类型,因为这些对象都是共享偏移信息的,访问它们只需要按照编译时确定的偏移量就可以了,而对于c++模版的支持,其实都是多份代码,本质上是相同的,JavaScript则不同,每个对象都是自我描述,属性和位置偏移信息都包含在自身的结构中

偏移信息查找:c++中查找偏移地址简单,都是在编译代码时对使用到某类型的成员变量直接设置偏移值,而对于JavaScript使用到一个对象则需要通过属性名匹配才能查找对应的值

[b]JavaScript引擎[/b]

JavaScript引擎是指能够将JavaScript代码处理并执行的运行环境,其主要包含一下几个部分:

编译器:主要工作是将源代码编译成抽象语法树,在某些引擎中还包含将抽象语法树转换成字节码

解释器:在某些引擎中,解释器主要是接收字节码,解释执行这个字节码,同时也依赖垃圾回收机制等

JIT工具:将字节码或者抽象语法树转换成本地代码

垃圾回收器和分析工具:它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效

[b]JavaScript引擎和渲染引擎[/b]

JavaScript引擎负责执行JavaScript代码,渲染引擎负责渲染网页,JavaScript引擎提供调用接口给渲染引擎,以便让渲染引擎使用JavaScript引擎来处理JavaScript代码并获取结果,此外JavaScript引擎需要能够访问渲染引擎构建的DOM树,所以JavaScript引擎通常需要提供桥接的接口,而渲染引擎则根据桥接接口来提供让JavaScript访问DOM的能力,下图表示了两种引擎之间的关系



V8引擎

[b]应用程序编程接口(API)[/b]

V8是一个开源项目,是一个JavaScript引擎的实现,它支持众多的操作系统,同时也能支持众多的硬件架构。V8所提供的应用编程接口它们在V8代码目录的include/V8.h中,其中一些主要的类如下:

各种各样的基础类:这里面包含对象引用类(如WeakReferenceCallbacks)、基本数据类型类(如Int32、Integer、Number、String、StringObject)和JavaScript对象(Object),这些都是基础抽象类,没有包含实现的实现,真正的实现在src目录中的“object.h/cc”中

Vaue:所有JavaScript数据和对象的基类,如上面的Integer、Number、String等

V8数据的句柄类:以上数据类型的对象在V8中有不同的生命周期,需要使用句柄来描述它们的生命周期,以及垃圾回收期如何使用句柄来管理这些数据,句柄类包括Local、Persistent和Handle

Isolate:这个类是一个V8引擎实例,包括相关状态信息、堆等,它是一个能够执行JavaScript代码的类,不能被多个线程同时访问,如果非要这么做的话需要使用锁,V8使用者可以使用创建多个该类的实例,但每个实例之间就像这个类的名字一样,都是孤立的

Context:执行上下文,包含内置的对象和方法,如print方法等,还包括JavaScript内置的库,如math等

Extension:V8的扩展类,用于扩展JavaScript接口,V8使用者基于该类来实现相应的接口,被V8引擎调用

Handle:句柄类,主要用来管理基础数据和对象,以便被垃圾回收器管理,主要有两个类型,一个是Local(Local类,继承自Handler类),另一个是Persistent(Persistent类,继承自Handler类),前者表示本地栈上的数据,所以量级比较轻,后者表示函数间的数据和对象访问

Script:用于表示被编译过的JavaScript源代码,V8的内部表示

HandleScope:包含一组Handle的容器类,帮助一次性删除这些Handle,避免重复调用

FunctionTemplate:绑定C++函数到JavaScript,函数模版的一个例子就是将JavaScript接口的C++实现绑定到JavaScript引擎

ObjectTemplate:绑定C++对象到JavaScript,对象模版的典型应用是Chromium中将DOM节点通过该模版包装成JavaScript对象

[b]接口使用实例[/b]

借鉴书中用例:



上图没有创建Isolate对象,此对象可以通过Isolate::GetCurrent()来获取,它会创建一个V8引擎实例,后面的操作都是在它提供的环境中来进行的,下面按语句编号进行分析:

1 建立一个域,用于包含一组Handler对象,便于释放它们

2 根据Isolate对象来获取一个Context对象,使用Handle来管理,Handle对象本身存放在栈上,而实际的Context对象保存在堆中

3 根据两个对象Isolate和Context来创建一个函数间使用的对象,所以使用Persistent类来管理,这里展示的是它们的用处和含义,在本里中不是必需的,其句柄和数据都单独存储在另外的地方

4 为Context对象创建一个基于栈的域,下面的执行步骤都是在该域中对应的上下文来进行的

5 从命令行参数读入JavaScript代码,也就是一段字符串

6 将字符串编译成V8的内部表示,并保存为一个Script对象

7 执行编译后的内部表示,然后获得生成的结果

[b]工作原理[/b]

[b]数据表示[/b]

在JavaScript中,只有基本数据类型Boolean、Number、String、Null和Undefined,其他数据都是对象,在V8中,数据的表示被分成两个部分,第一部分是数据的实际内容,它们是变长的,而且内容的类型也是不一样的,如String、对象等,第二部分是数据的句柄,句柄的大小是固定的,句柄中包涵指向数据的指针,之所以要这样设计是因为V8需要进行垃圾回收,并需要移动这些数据内容,如果直接使用指针的话会出现问题或需要比较大的开销,使用句柄的话就不存在这些问题,只需要将句柄中的指针修改即可,使用这使用的还是句柄,本身没有发生变化,除了极少数的数据例如整形数据,其他的内容都是从堆中申请内存来存储它们,因为Handler本身能够存储整形,同时也为了快速访问,而对于其他类型,受限于Handle的大小和变长等原因,都存储在堆中。JavaScript对象的实现在V8中包含3个成员,第一个是隐藏类的指针,这是V8为JavaScript对象创建的隐藏类,第二个指向这个对象包含的属性值,第三个指向这个对象包含的元素。

[b]V8工作过程[/b]

V8的工作过程包括两个阶段,第一是编译,第二是运行,在V8中存在延迟思想,这使得JavaScript代码的编译直到运行的时候被调用到才会发生,这样可以减少时间开销。对于编译阶段,下图展示了V8从源代码到本地代码的过程:



不同与JavaScriptCore引擎,V8引擎并不将抽象语法树转变成字节码或者其他中间表示,而是通过JIT编译器的全代码生成器从抽象语法树直接生成本地代码,这样做的原因在于减少抽象语法树到字节码的转换,这样虽然可以提高优化的可能,但是也存在一些不足,第一是在某些JavaScript使用场景中使用解释器更为合适,因为没有必要生成本地代码,第二是没有中间表示会减少优化的机会,因为缺少一个中间表示层。

下图说明了V8引擎编译JavaScript生成本地代码使用了哪些主要的类和过程:



Script:表示的是JavaScript代码,既包含源代码,又包含编译之后生成的本地代码,因此即是编译入口,又是运行入口

Compiler:编译器类,辅助Script类来编译生成代码,主要起一个协调者的作用,会调用解释器Parse来生成抽象语法树和全代码生成器,为了抽象语法树生成本地代码

Parser:将源代码解释并构建成抽象语法树,使用AstNodeFactory类来创建它们,并使用Zone类来分配内存

AstNode:抽象语法树节点类,是其他所有节点的基类,包含很多的子类,后面会针对不同的子类生成不同的本地代码

AstVisitor:抽象语法树的访问者类,基于Visitor设计模式来设计,主要用来遍历异构的抽象语法树

FullCodeGenerator:AstVisitor类的子类,通过遍历抽象语法树来为JavaScipt生成本地可执行代码

对于编译器的全代码来说,本地代码跟具体的硬件平台密切相关,因此它使用多个后端来生成实际的代码,如下图所示,V8引擎至少包含四个跟平台相关的后端,用于生成不同平台上的本地汇编代码。



当代码生成器遍历AST树的时候,FullCodeGenerator会为每个节点生成相应的汇编代码,不过没有了全局的视图,因此没有为节点之间考虑可能的优化,在不同的平台上,FullCodeGenerator有不同的实现。在V8生成本地代码之后为了考虑性能,通过数据分析器(Profiler)来采集一些信息,以帮助决策哪些本地代码需要优化,以生成效率更高的本地代码,这是一个逐步改进的过程,同时V8中当发现本地优化后的代码性能并没有提高甚至还有所降低的时候,V8能够退回到原来的代码,这些都是在运行阶段涉及到的。

下面介绍运行阶段的代码,以下是主要的类:

Script:包含编译之后的生成的本地代码,运行代码的入口

Execution:运行代码的辅助类,包含一些重要的函数,例如call,它辅助进入和执行Script中的本地代码

JSFunction:需要执行的JavaScript函数表示类

Runtime:运行这些本地代码的辅助类,它的功能主要是提供运行时各种各样的辅助函数,包括但是不限于属性访问、类型转换、编译、算数、位操作、比较、正则表达式等

Heap:运行本地代码需要使用内存堆

MarkCompactCollector:垃圾回收机制的主要实现类,用来标记(Mark)、清除(Sweep)和整理(Compact)等基本的垃圾回收过程

SweeperThread:负责垃圾回收的线程

下图描述了V8支持JavaScript代码运行的主要类:



结合这些类,V8引擎是按照下图描述的过程来执行的,调用发生在图中的三个阶段,第一是延迟编译,为CompileLazy这个函数的调用,在V8中函数是一个基本单位,当某个JavaScript函数被调用的时候,属于该函数的本地代码就会生成,具体的工作方式是V8查找该函数是否已经生成本地代码,如果已经生成,那么直接调用,否则V8引擎会触发生成本地代码,目的是节约时间,减少去处理那些用不到的代码时间,第二是下图1.2.3,这时执行编译后的代码就会为JavaScript构建JS对象,这需要Runtime类来辅助创建对象,并需要从Heap类分配内存,第三是1.2.4阶段,此阶段需要借助Runtime类中的辅助函数来完成一些功能,如属性访问、类型转换等。



优化回滚

为了性能的考虑,编译器通常会做比较乐观和大胆的预测,编译器会认为某些代码比较稳定,变量类型不会发生改变,所以能够生成高效的本地代码,这是理想情况,实际引擎会发现一些变量的类型已经发生变化,在这种情况下,V8使用一种机制来将它做的这些错误决定回滚到之前的情况,这个过程称为优化回滚。下面举个例子来说明:

var counter = 0;
function Func(x,y){
counter++;
if (counter <= 1000000) {
return counter;
}
var unknown = new Date();
printf(unknown);
}


函数Func被调用多次后V8引擎可能会触发Crankshaft编译器来生成优化代码,优化的代码认为示例代码的类型等信息都已经被获知,但事实是对于代码中的unknown变量的类型还一无所知,在这种情况下V8只能将该端代码回滚到一个通用的状态。

隐藏类和内嵌缓存

V8实用类和编译位置思想,将本来需要通过字符串匹配来查找属性值的算法改进为使用类似C++编译器的偏移位置的机制来实现,这就是隐藏类,隐藏类将对象划分成不同的组,对于相同的组,也就是该组内的对象拥有相同的属性名和属性值的情况,将这些属性名和对应的偏移值保存在一个隐藏类中,组内的所有对象共享信息,同时也可以识别属性不同的对象,下图举例加以说明:



JavaScript没有办法定义类型,上图代码部分创建了两个对象a和b,这两个对象包含相同的属性名,在V8中它们被归为同一个组,也就是一个隐藏类,这些属性在隐藏类中有相同的偏移值,这样,对象a和b可以共享这个类型信息,当访问这些对象属性的时候,根据隐藏类的偏移值就可以知道它们的位置并进行访问,由于JavaScript是动态类型语言,假如上述代码之后加入b.z=2。那么b所对应的将是一个行的隐藏类,这样a和b将属于不同的组。在理解了V8的隐藏类之后,以下面代码来了解一下是如何使用隐藏类来高效访问对象属性的,function add(a) {return a.x;},首先看最基本的情况,访问对象属性的过程是这样的:首先获取隐藏类的地址,然后根据属性名查找偏移值,计算该属性的地址,不过,这个过程比较费时,实际上的情况可能要好很多,因为很多情况下该函数中的参数a可能是同一种类型,那么可以使用内嵌缓存机制,它可以避免方法和属性被存取的时候出现的因哈希表查找而带来的问题,该机制的基本思想是使用之前查找的结果缓存起来,也就是说V8可以将之前查找的隐藏类和偏移值保存下来,当下次查找的时候,首先比较当前对象是否也是之前保存的隐藏类,如果是的话可以直接使用之前缓存的偏移值,从而减少查找表的时间。如果该函数中的对象a出现多个类型,那么缓存失误的几率就会高很多,当出现缓存失误的时候,V8可以按照上面说的退回到之前的方式来查找哈希表,但是因为效率问题,V8会在缓存失败后通过对象a的隐藏类来查找该类中有无一段代码,这段代码可以快速查找对象,这段代码保存在a对象的隐藏类对应的表中,所以如果该端代码已经生成,就同样可以较快的实现属性值的查找。

if (a->hiddenClass() == cachedClass){
return a->properties[cachedOffset];
} else {
// 退回到原来的方法
}


内存管理

V8的内存管理部分主要有两点,1 V8内存的划分;2 V8对于JavaScript代码的垃圾回收机制。对于内存的划分,首先看Zone类,它的特点是管理一系列的小块内存,如果用户想使用一系列的小内存,并且这些小内存的生命周期类似,这时可以使用一个Zone对象,这些小内存都是从Zone对象中申请的,Zone对象首先自己申请一块内存,然后管理和分配一些小内存,当一块小内存被分配之后,不能够被Zone回收,只能一次性回收Zone分配的所有小块内存,例如抽象语法树的内存分配和使用,在构建抽象语法树之后,会生成本地代码,然后抽象语法树的内存在这之后被一次性全部收回,效率高,但是该机制有一个严重的缺陷,假如这一过程需要很多内存,那么Zone就需要为系统分配大量的内存,但是又不能够释放,这会导致系统出现需要过多的内存而导致内存不够的情况。其次是堆,V8使用堆来管理JavaScript使用的数据,以及生成的代码、哈希表等,为了更方面的实现垃圾回收,同很多虚拟机一样,V8将堆分成三个部分,第一个是年轻分代,第二个是年老分代,其中还分成多个子部分,第三个是大对象保存的空间。对于年轻分代,主要是为新创建的对象分配内存空间,因为年轻分代中的对象较容易被回收,为了方面垃圾回收,可以使用复制方式,将年轻分代分成两半,一半用来分配,另外一半在回收的时候负责将之前保留的对象复制过来,对于年轻分代,经常需要进行垃圾回收,对于年老分代,主要是根据需要将年老的对象、指针、代码等数据使用的内存较少做垃圾回收,而对于大对象空间,主要是用来为那些需要较多内存的大对象进行分配回收,当然同样可能包含数据和代码等分配的内存,需要注意的是每个页面只分配一个对象。对于垃圾回收,因为使用了分代和大数据的内存分配,V8需要使用精简的算法,用来标记那些还被引用的对象,然后消除那些没有被标记的对象,最后整理和压缩那些还需要保存的对象。

快照

在V8引擎开始启动的时候,需要加载很多内置的全局对象,同时也要建立内置的函数,如Array、String、Math等,为了让引擎更加整洁,加载对象和建立函数等任务都是使用JS文件来实现的,V8引擎负责提供机制来支持,就是在编译和执行输入的JavaScript代码之前,先加载它们,根据前面介绍的V8引擎需要编译和执行这些内置的JS代码,同时使用堆等来保存执行过程中创建的对象、代码等,这些需要较多的时间,为此,V8引入了快照机制,快照机制就是将这些内置的对象和函数加载之后的内存保存并序列化,序列化的结果很容易被反序列化,经过快照机制的启动时间,可以缩减几毫秒,在编译的时候打开选项”snapshot=on“可以让V8支持快照机制,在V8中,mksnapshot工具能够帮助生成快照,快照机制同时也能够将一些开发者认为需要的js文件序列化,以减少以后处理的时间。

JavaScriptCore引擎

架构和模块

数据表示

JavaScriptCore引擎使用句柄来表示数据,对于简单类型的数据则直接包含在句柄中,对于对象来说,则使用指针来指向数据在堆中的位置,同V8引擎不同,在32位和64位机器上,句柄都是使用64位来表示的。首先在32位平台上,每个句柄都是使用两个32位数据来表示,对于整数、布尔和指针而言,前面32位用来标记它们,后面32位用来表示这些数据,对于双浮点,前32位在区间FFFFFFF8~00000000都是用来表示浮点类型,可能稍微比原来双浮点表示范围小一些,但是这个范围已经足够使用了,同样在64位机器上,因为标记指针需要64位,只好使用前面16位,而后面48位表示地址,同V8引擎相比,JavaScriptCore引擎因为在32位上使用64位来表示句柄,所以除了小整数之外,对于浮点类型同样可以不需要访问堆中的数据,当然,缺点就是每个句柄都需要2倍的内存空间。

模块

JavaScriptCore引擎与V8相比有很多不同之处,最典型的就是使用了字节码的中间表示,并加入了多层JIT编译器帮助改善性能,不停的优化编译之后的本地代码,具体表现为:

第一,不同于V8引擎,JavaScriptCore不是从抽象语法树生成本地代码,而是生成平台无关的字节码,JavaScriptCore引擎自己定义了一套字节码规范,该字节码与平台无关,而且有了该字节码,JavaScriptCore就可以基于其进行很多在抽象语法树之上不能或者很难做到的优化,不同于V8,在这之后,因为有了字节码,所以JavaScriptCore就不在需要JavaScript源代码,而V8使用Crankshaft编译器进行进一步优化,则需要继续从JavaScript源代码重新开始。



第二,在字节码之后,JavaScriptCore依然包含了字节码解释器,这点类似与Java虚拟机中的解释器,它们能够解释字节码然后生成结果,而不同于Java虚拟机的解释器是,JavaScriptCore是基于虚拟寄存器的虚拟机,而Java是基于栈式的虚拟机,因为一些JavaScript代码不需要经过很强的优化,只需要直接执行即可,复杂的处理可能带来额外的开销反而抵消优化带来的全部好处,同时在字节码执行期间,信息收集器会收集热点函数,以方便之后的JIT编译器做之后的优化工作。



第三,JavaScriptCore引擎在获悉热点函数后,需要对它们进行优化,就会使用到简单JIT编译器,该编译器根据信息收集器中的信息,将对应函数的字节码解释成本地代码,不仅因为时间问题,而且并不是所有代码都合适做深层此的优化,而是直接做转换,下图描述了这一过程,在实行这些本地代码的时候,会有信息收集器2来收集代码并作进一步的优化。



第四,简单的JIT编译器并不能满足性能的要求,特别是对V8的Crankshaft编译器来说,性能的差距就显示出来了,为了提高性能,JavaScriptCore又引入了DFG(Data-Flow Graph)JIT编译器,该编译器是在字节码基础上,生成基于SSA(Static Single Assignment)的中间表示(IR),当然具体哪些字节码需要重新生成优化的本地代码,就依赖之前的信息收集器2,如下图,优化后的本地代码相比,对于性能有了很好的提升



第五,后期会将LLVM技术引入到JavaScriptCore,LLVM是一个编译器,能够将多个不同的前端语言转化成不同的后端本地代码,如下图:



该编译器在前端和后端都能做优化,这些优化是可配置的,同时,随着项目越来越成功,加入的优化也越来越多,JavaScriptCore希望将LLVM编译器的中间表示引入其中,这样将很容易将这些优化使用在该引擎中



这一过程是基于DFG JIT中间表示开始的,为了节省时间,使用了并行编译算法,之后,生成LLVM的中间表示,这样就可以使用LLVM中间表示之后的众多优化,而且可以按需配置它们,这一过程仅仅对于那些最热点的函数使用,因为其层次太多,消耗的时间更多,所以慎用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息