JavaScript引擎分析

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. 优化回滚

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

原文发布于微信公众号 - 小时光茶社(gh_7322a0f167b5)

原文发表时间:2016-08-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

通用的序列号生成器库

正如文章《通用的业务编号规则设计实现(附源码)》 文章里需要一个多实例和线程安全的序列化生成器,在SQL Server 2012+ 版本 有一个通过.NET程序...

1895
来自专栏韩伟的专栏

框架设计原则和规范(完)

祝大家圣诞节快乐!有事没事别出门,外面太!挤!了! 此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,今天推送最后一章。 1. 什么是好...

2874
来自专栏FreeBuf

以UPX漏洞为例介绍整数溢出(基础篇)

*本文原创作者:tocttou,本文属FreeBuf原创奖励计划,未经许可禁止转载

1142
来自专栏Ryan Miao

YAML 语法

YAML 语法 来源:yaml 这个页面提供一个正确的 YAML 语法的基本概述, 它被用来描述一个 playbooks(我们的配置管理语言). 我们使用 YA...

3859
来自专栏史上最简单的Spring Cloud教程

Openresty最佳案例 | 第2篇:Lua入门

什么是lua Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能...

7429
来自专栏禹都一只猫博客

符合语言习惯的Python编程技巧

712
来自专栏腾讯NEXT学位

JavaScript引擎分析

? 一.JavaScript简介 JavaScript是一种动态类型的脚本语言;在1995年时,由Netscape公司的Brendan Eich,在网景导航者...

3432
来自专栏mwangblog

python开始

1172
来自专栏木宛城主

Unity应用架构设计(7)——IoC工厂理念先行

一谈到 『IoC』,有经验的程序员马上会联想到控制反转,将创建对象的责任反转给工厂。IoC是依赖注入 『DI』 的核心,大名鼎鼎的Spring框架就是一个非常...

2807
来自专栏向治洪

类加载器原理

1.类的加载过程   JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示: ?...

2118

扫码关注云+社区

领取腾讯云代金券