写点什么

JavaScript 引擎分析

  • 2019-08-22
  • 本文字数:4121 字

    阅读完需:约 14 分钟

JavaScript引擎分析

一.JavaScript 简介

JavaScript 是一种动态类型的脚本语言;在 1995 年时,由 Netscape 公司的 Brendan Eich,在网景导航者浏览器上首次设计实现而成。因为 Netscape 与 Sun 合作,Netscape 管理层希望它外观看起来像 Java,因此取名为 JavaScript。


JavaScript 脚本语言具有以下特点:


(1)脚本语言。JavaScript 是一种解释型的脚本语言,是在程序的运行过程中逐行进行解释执行,不需要预编译。;而 Java、C++等语言需要先编译后执行;


(2)动态性。JavaScript 能够动态修改对象的属性,没有办法在编译的时候知道变量的类型,只有在运行的时候才能确定;而 Java、C++等都是静态类型语言,他们在编译的时候就能够知道每个变量的类型;


(3)跨平台性。JavaScript 脚本语言不依赖于操作系统,仅需要浏览器的支持。可以在多种平台下运行(如 Windows、Linux、Mac、Android、IOS 等)

二.JavaScript 与 Java 语言区别

从上面介绍的 JavaScript 语言特点会发现 JavaScript 的效率会比 Java、C++低很多;看以下这个实例:



当 JavaScript 引擎分析到该段代码的时候,根本不知道 a 和 b 是什么类型,唯一的办法就是运行的时候根据实际传过来的对象再来计算,这显然会导致严重的性能问题;



当编译上面 Java 代码的时候,根据右边类型 Class1 的定义,获取对象 a 的属性 x 的时候,其实就是对象 a 的地址,大小是一个整形。同时获取对象 b 的属性 y 的时候,其实就是对象 b 的地址加上 4 个字节,这些都是在生成本地代码的时候确定的,无需再运行本地代码的时候再决定他们的地址和类型是什么,这显然能够节省时间;


再看一下两者分别是怎样存储对象 a 和 b 的:


对于传统的 JavaScript 解释器来说,因为不知道 a 和 b 的具体类型,就用属性名-属性值对来保存,之后访问对象的属性值时就需要通过属性名匹配来获取对应的值;对象 b 也是同样的结果来保存相同的属性;随着对象的增多,这显然带来了巨大的空间浪费;



而上面的 Java 代码在编译时就确定了类 Class1 的成员类型,访问 x 就是对象 a 的地址,y 就是 a 的地址加上 4 个字节;所以字符“x”和“y”运行时都不在需要;因为不再需要额外的查找这些属性地址的工作;



从上面实例可以看到 JavaScript 和 Java 语言区别包括以下几个部分:


编译确定位置: Java 有编译和执行两个阶段,位置的偏移信息都是在编译器编译的时候决定的,当 java 生成本地代码之后,对象的属性和偏移信息都计算完成;而 JavaScript 没有类型,只有在对象执行创建的时候才确定这些信息,而且 JavaScript 语言能够在执行的时候修改对象的属性。


偏移信息共享: Java 有类型定义,所有的对象都是共享偏移信息的;访问他们只需要按照编译时确定的偏移量即可。JavaScript 则不同,每个对象都有自我描述,属性和位置偏移信息都包含在自身的结构中。


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


Java 语言有明显的两个阶段:编译和运行,如下图所示:



Java 代码经过编译器编译之后生成的是字节码,字节码是跨平台的一种中间表示,不同于本地代码。该字节码于平台无关,能够在不同的操作系统上运行。在运行字节码阶段,Java 的运行环境是 Java 虚拟机加载字节码。Java 虚拟机一般都引入 JIT 技术来将字节码转变成本地代码来提高执行效率。第一阶段对时间要求不严格,第二阶段对每个步骤所花费的时间非常敏感,时间越短越好。


JavaScript 语言的编译和执行都是在运行阶段执行的,如下图所示:



因为都是在代码运行过程中来处理这些步骤,所以每个阶段的时间越短越好,而且每引入一个阶段都是额外的时间开销;所以一个 JavaScript 引起主要包含以下几个部分:


编译器: 主要工作是将源代码编译成抽象语法树;


解释器: 主要是接受字节码,解释执行这个字节码;


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


垃圾回收期和分析工具(Profiler): 负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能;

三.V8 引擎介绍

V8 是一个 JavaScript 引擎实现的开源项目,最开始由一些语言学家设计出来,后被 Google 收购,成为了 JavaScript 引擎和众多相关技术的引领者。V8 支持众多的操作系统,包括 Windows、Linux、Android、Mac OS X 等;同时它也能够支持众多的硬件架构 IA32、X64、ARM、MIPS 等,他将主流软硬件平台一网打尽,由于它是一个开源项目,开发者可以自由使用它的强大能力,目前炙手可热的 NodeJs 项目就是基于 V8 项目研发的。

1.调用 V8 编程接口的例子和对应的内存管理方式:


第一条语句:表示建立一个域,用于包含一组 Handle 对象,便于管理和释放他们;


第二条语句:根据 Isolate 对象来获取一个 Context 对象,使用 Handle 来管理。Handle 对象本身存放在栈上,而实际的 Context 对象保存在堆中。


第三条语句:根据两个对象 Isolate 和 Context 来创建一个函数间使用的对象,使用 Persistent 类来管理;


第四条语句:表示为 Context 对象创建一个基于栈的域,下面的执行步骤都是在该域中对应的上下文中来进行的;


第五条语句:读入一段 JavaScript 代码;


第六条语句:将代码字符串编译成 V8 的内部表示,并保存成一个 Script 对象;


第七条语句:执行编译后的内部表示,获得生成的结果;

2.V8 的编译:


首先通过编译器将源代码编译成抽象语法树,不同于 JavaScriptCore 引擎,V8 引擎并不将抽象语法树转变成字节码,而是通过 JIT 编译器的全代码生成器从抽象语法树直接生成本地代码;


其过程中的主要类图如下:



Script:表示的是 JavaScript 代码,既包含源代码,又包含编译之后生成的本地代码,所以它既是编译入口,又是运行入口;


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


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


AstNode:抽象语法树节点类,是其他所有节点的基类;


AstVisitor:抽象语法树的访问者类,主要用来遍历抽象语法树;


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

3.V8 运行

V8 运行阶段的主要类图如下:



Script:前面介绍过,包含编译之后生成的本地代码,运行代码的入口;


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


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


Runtime:运行本地代码的辅助类,主要提供运行时各种辅助函数;


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


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


SweeperThread:负责垃圾回收的线程;


V8 中代码的执行过程如下图:


四.V8 引擎所做优化

1.优化回滚

Crankshaft 编译器主要针对热点函数进行优化,它是基于 JS 源码分析的,而不是本地代码。为了性能考虑 Crankshaft 编译器会进行一些乐观的预测,认为这些代码比较稳定,变量类型不会发生变化,所以能够生成高效的本地代码;然而进行优化之后,V8 发现并不是最优的,会执行优化回滚操作。

2.隐藏类

将对象划分成不同的组,相同的组内对象拥有相同的属性名和属性值,组内的所有对象贡献该信息;



实例中对象 a 和 b 包含相同的属性名,V8 就会把他们归为同一个组,也就是隐藏类;这些属性在隐藏类中有相同的偏移值,这样,对象 a 和 b 可以共享这个类型信息,当访问这些对象属性的时候,根据隐藏类的偏移值就可以知道他们的位置并进行访问。


###3.内存管理


V8 使用堆来管理 JavaScript 使用的数据,以及生成的代码,哈希表等;为了更方便的实现垃圾回收,同很多虚拟机一样,V8 将堆分成三个部分,第一个是年轻分代,第二个是年老分代,第三个是大对象保留的空间。如下图:


4.快照(Snapshot)

V8 引擎开始启动的时候,需要加载很多内置的全局对象,同时也要建立内置的函数,比如 Array、String、Math 等;为了让引擎更加整洁,加载对象与建立函数等任务都是使用 JS 文件来实现的,V8 引擎负责在编译和执行输入的 JavaScript 代码之前,先加载他们;


快照机制就是将一些内置的对象和函数加载之后的内存保存并序列化;序列化之后的结果很容易被发序列化,经过快照机制的启动时间,可以缩短启动时间;快照机制也能够将开发者认为需要的 JS 文件序列化,减少以后处理的时间;

5.绑定和扩展

V8 提供两种机制来扩展引擎的能力,第一是 Extension 机制,就是通过 V8 提供的基类 Extension 来达到扩展 JavaScript 能力的目的;第二是绑定,使用 IDL 文件或者接口文件来生成绑定文件,然后将这些文件同 V8 引擎代码一起编译。

五.实践 – 写 JavaScript 需要注意地方

1.不要破坏隐藏类


建议:在构造函数中初始化所有对象成员,不要在以后更改类型;以相同的顺序初始化对象成员。

2.数据表示

在 V8 中,数据的表示分成两个部分,第一个部分是数据的实际内容,他们是变长的,第二部分是数据的句柄,句柄的大小是固定的,句柄中包含指向数据的指针。为什么要这样设计呢?主要是因为 V8 需要进行垃圾回收,并需要移动这些数据内容,如果直接使用指针的话就会出问题或者需要比较大的开销,使用句柄的话就不存在这些问题,只需要将句柄中的指针修改即可。


具体的定义如下:



一个 Handler 的大小是 4 字节(32 位机器),整数直接从 value_中获取值,而无需从堆中分配,然后分配一个指针指向它,这可以减少内存的使用并增加数据的访问速度。


所以:对于数值来说,只要能够使用整数的,尽量不要使用浮点数。

3.数组初始化


建议:


初始化使用数组常量小型固定大小的数组


不要储存在数字数组非数字值(对象)


不要删除数组中的元素,尤其是数字数组


不要装入未初始化或删除元素

4.内存

对引用不再使用的对象的变量设置为空(a = null),引入 delete 关键字,删除无用对象。

5.优化回滚

不要书写出触发优化回滚的代码,否则会大幅降低代码的性能;执行多次之后,不要出现修改对象类型的语句。


本文转载自公众号小时光茶舍(ID:gh_7322a0f167b5)。


原文链接:


https://mp.weixin.qq.com/s/nV75KBmRGpcE7Pn5Z-HUFg


2019-08-22 10:502155

评论

发布
暂无评论
发现更多内容
JavaScript引擎分析_文化 & 方法_厉心刚_InfoQ精选文章