本系列的第一篇文章重点介绍了引擎,运行时和调用栈的概述。第二篇文章将深刻V8的JavaScript引擎的内部。咱们还会提供一些关于如何编写更好的JavaScript代码的技巧。编程
JavaScript引擎是执行JavaScript代码的程序或解释器。JavaScript引擎能够用标准解释器(interpreter)或即时编译器(just-in-time compiler)来实现,即时编译器以某种形式将JavaScript代码编译为字节码。segmentfault
流行的JavaScript引擎:数组
V8引擎是由Google构建的,用C++开发而且开源,与其它的引擎不一样的是,V8仍是Node.js的运行时环境。浏览器
V8最初设计用于提升浏览器内部JavaScript执行的性能。为了得到速度,V8将JavaScript代码转换为更高效的机器代码(machine code),而不是使用解释器。它经过实现JIT(Just-In-Time)编译器(如SpiderMonkey或Rhino,等许多现代JavaScript引擎)将JavaScript代码编译为机器代码。这里的主要区别在于V8不生成字节码或任何中间代码。缓存
在V8引擎的v5.9版本出来以前,V8有两个编译器:
full-codegen:一个简单并且速度很是快的编译器,能够生成简单且相对较慢的机器代码。
Crankshaft:一种更复杂(Just-In-Time)的优化编译器,能够生成高度优化的代码。架构
V8引擎还在内部使用多个线程:编程语言
当第一次执行JavaScript代码时,V8利用full-codegen,直接将解析的JavaScript翻译成机器代码而无需任何转换。这使它能够很是快速地开始执行机器代码。请注意,V8不使用中间字节码表示法,不须要解释器。ide
当您的代码运行一段时间后,Profiler线程已经收集了足够的数据以肯定哪一种方法应该进行优化。函数
接下来,Crankshaft优化从另外一个线程开始。它将JavaScript抽象语法树翻译为称为Hydrogen的高级静态单分配(SSA)表示,并尝试优化该hydrogen图。大多数优化都是在这个级别完成的。布局
第一次优化是提早尽量多地嵌入代码。 内联是将被调用函数的主体替换为调用网站(调用该函数的代码行)的过程。 这个简单的步骤可让如下优化变得更有意义。
JavaScript是一种基于原型的语言:没有类,对象的建立是经过克隆实现的。JavaScript也是一种动态编程语言,它意味着属性能够在实例化后轻松添加或从对象中移除。
大多数JavaScript解释器使用字典式结构(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得检索JavaScript中的属性的值比在Java或C#等非动态编程语言中的计算更昂贵。在Java中,全部对象属性都是在编译以前由固定的对象布局肯定的,而且不能在运行时动态添加或删除(固然,C#的动态类型是另外一个主题)。所以,属性的值(或指向这些属性的指针)能够做为连续缓冲区存储在内存中,每一个值之间都有一个固定偏移量。偏移量的长度能够根据属性类型轻松肯定,但在运行时能够更改属性类型的JavaScript中不可行。
因为使用字典查找内存中对象属性的位置效率很是低,所以V8使用不一样的方法:隐藏类。隐藏类的工做方式与Java等语言中使用的固定对象布局(类)相似,除了它们是在运行时建立的。如今,让咱们看看他们实际的样子:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
当“new Point(1, 2)”被执行时, V8引擎会建立一个名为C0的隐藏类。
因为Point还未定义任何属性,所以“C0”为空。
一旦执行了第一条语句“this.x = x”(在“Point”函数内部),V8将建立第二个隐藏类“C1”,它基于“C0”。“C1”描述了能够找到属性x的存储器中的位置(相对于对象指针)。在这种状况下,“x”存储在偏移量0处,这意味着在内存中将点对象视为连续缓冲区时,第一个偏移量将对应于属性“x”。 V8还将用“类别转换”更新“C0”,该类别转换指出若是将属性“x”添加到点对象,隐藏类应从“C0”切换到“C1”。 下面的点对象的隐藏类如今是“C1”。
每次将新属性添加到对象时,旧的隐藏类都会使用到新隐藏类的转换路径进行更新。隐藏类转换很是重要,由于它们容许隐藏类在以相同方式建立的对象之间共享。若是两个对象共享一个隐藏类并向它们添加了相同的属性,则转换将确保两个对象都接收到相同的新隐藏类以及随附的全部优化代码。
当执行语句“this.y = y”(一样,在“this.x = x”语句以后的Point函数内部)时,将重复此过程。
建立一个名为“C2”的新隐藏类,将类转换添加到“C1”,指出若是将属性“y”添加到Point对象(已包含属性“x”),则隐藏类应更改成 “C2”,点对象的隐藏类更新为“C2”。
隐藏类转换取决于将属性添加到对象的顺序。 看看下面的代码片断:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 5); p2.b = 7; p2.a = 8;
如今,您可能认为对于p1和p2,将使用相同的隐藏类和转换。事实上却不是。对于“p1”,首先添加属性“a”,而后添加属性“b”。然而,对于“p2”,首先分配“b”,而后是“a”。 所以,因为不一样的转换路径,“p1”和“p2”以不一样的隐藏类结束。在这种状况下,以相同顺序初始化动态属性好得多,以便隐藏的类能够重用。
V8利用另外一种技术来优化称为内联缓存的动态类型化语言。内联缓存依赖于观察到对相同方法的重复调用倾向于发生在相同类型的对象上。在这里能够找到关于内联缓存的深刻解释。
咱们将讨论内联缓存的通常概念(若是您没有时间经过上面的深刻解释)。
那么它是怎样工做的? V8维护一个对象类型的缓存,这些对象在最近的方法调用中做为参数传递,并使用这些信息来预测未来做为参数传递的对象的类型。若是V8可以对传递给方法的对象的类型作出很好的假设,那么它能够绕过肯定如何访问对象属性的过程,而是使用之前查找存储的信息到对象的隐藏课程。
那么隐藏类和内联缓存的概念如何相关?不管什么时候在特定对象上调用方法,V8引擎都必须执行对该对象的隐藏类的查找,以肯定访问特定属性的偏移量。在相同隐藏类的两次成功调用以后,V8省略了隐藏类查找,并简单地将该属性的偏移量添加到对象指针自己。对于该方法的全部将来调用,V8引擎都假定隐藏的类没有更改,并使用从之前的查找存储的偏移量直接跳转到特定属性的内存地址。这大大提升了执行速度。
内联缓存也是为何相同类型的对象共享隐藏类很是重要的缘由。若是您建立两个具备相同类型和不一样隐藏类的对象(就像咱们以前的示例中那样),V8将没法使用内联缓存,由于即便这两个对象的类型相同,它们对应的隐藏类为其属性分配不一样的偏移量。
一旦Hydrogen图被优化,Crankshaft将其下降到称为Lithium的较低级表示。大部分的Lithium实施都是特定于架构的。寄存器分配发生在这个级别。
最终,Lithium被编译成机器码。而后发生其余事情,称为OSR:堆栈替换。在咱们开始编译和优化那些耗时较长的方法以前,咱们可能会运行它。V8不会忘记它刚刚缓慢执行的内容,以再次优化版本开始。相反,它会转换咱们拥有的全部上下文(堆栈,寄存器),以便咱们能够在执行过程当中切换到优化版本。这是一项很是复杂的任务,考虑到除了其余优化以外,V8最初仍是将代码内联。 V8不是惟一可以作到的引擎。
有一种叫作去最佳化的保护措施能够作出相反的转变,并在引擎的假设再也不成立的状况下恢复到非优化的代码。
对于垃圾收集,V8采用了传统的标记清除方式来清理老一代。标记阶段应该中止JavaScript执行。为了控制GC成本并使执行更加稳定,V8使用增量标记:不是遍历整个堆,而是试图标记每一个可能的对象,它只走过堆的一部分,而后恢复正常执行。下一个GC中止将从先前堆走过的地方继续。这容许在正常执行期间很是短的暂停。如前所述,扫描阶段由单独的线程处理。
随着2017年早些时候发布V8 5.9,引入了新的执行流程。这个新的管道在实际的JavaScript应用程序中实现了更大的性能改进和显着的内存节省。
新的执行流程创建在Ignition,V8的解释器和TurboFan,V8的最新优化编译器之上。
您能够查看V8团队关于此主题的博客文章。
自从V8.5版本问世以来,V8团队一直在努力跟上新的JavaScript语言特性,而V8团队已经再也不使用V8版本的full-codegen和Crankshaft(自2010年以来服务于V8的技术)。这些功能须要进行优化。
这意味着总体V8将有更简单和更可维护的架构。
这些改进仅仅是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这将在将来几年提高JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。
最后,这里有一些关于如何编写优化的,更好的JavaScript的技巧和窍门。 您能够轻松地从上述内容中获取这些内容,可是,为了方便起见,如下是摘要: