Skip to content

V8 引擎 #49

@coconilu

Description

@coconilu

概述

V8引擎编译相关类:
compile

V8引擎运行相关类:
run

上面的图片摘自《WebKit 技术内部》

V8 引擎作为JS引擎中的佼佼者,主要有如下的方面改进:

1. 延迟编译

V8 有一个重要的特点就是延迟(deferred)思想,使得很多 JavaScript 代码的编译直到运行的时候才会进行,这样可以减少很多时间开销。比如很多回调(callback)都是依赖异步框架和事件循环的,它们都是等到时机成熟了之后才会被执行。

2. 函数优化和优化回滚

V8 有两个编译器:

一个非常简单并且非常快的编译器用于将 js 编译成简单但是很慢的机械码,叫做 full-codegen
另一个是非常复杂的实时优化编译器,编译高性能的可执行代码,叫做 Crankshaft

最开始执行你的代码的时候,V8 开始使用 full-codegen,full-codegen 直接将 JS 代码解释成机械码,没有做任何转化。这可以让 V8 快速执行机械码。注意,V8 并不使用中间字节码,因此也就不再需要转译处理。当你的代码被执行的时候,profiler 线程有足够的数据来找出哪些方法需要被优化。

编译器通常会做比较乐观和大胆的预测,那就是认为这些代码比较稳定,比如函数的入参的类型是不会改变的,所以生成高效的本地代码。但是如果某次调用该函数的时候,入参的类型改变了,那么就会把本地代码回滚到原代码,这就是优化回滚。

3. 数据表示

V8把JS代码里的每一个定义的变量分为三部分,第一部分是变量名;第二部分是数据的句柄,变量名指向的是数据的句柄,句柄的大小是固定的;第三部分是数据的实际内容。这样做是因为,JS是动态类型的,由句柄存储数据的实际类型,并指向实际内容的地址,也利于V8进行垃圾回收。

句柄的后两位用来表示数据是整数(00)或其它(01)。如果句柄是指向对象的,在V8中对象内部包含3个成员。第一个是指向隐藏类的指针,第二个是指向属性值表的指针,第三个是指向元素表的指针。

4. 隐藏类和内嵌缓存

大多数 JavaScript 解释器使用类似字典的结构 (基于散列函数) 去存储对象属性值在内存中的位置,查找对象属性的位置效率很低。

V8使用类和偏移位置思想,将本来需要通过字符串匹配来查找属性值的算法改进为使用类似C++编译器的偏移位置的机制来实现,这就是隐藏类(Hidden Class)。

隐藏类将对象划分成不同的组,它存储着同组内的对象的属性名和对应的偏移位置,对于同组(该组内的对象拥有相同的属性名和属性值的类型)内的所有对象共享这些信息。

就算通过隐藏类已经可以提高访问属性值的效率,但是还可以做得更好,就是把查找过的结果信息缓存起来,如果当前对象和之前的对象是同一个隐藏类,那么就可以省去在隐藏类里查找信息的功夫。这就是内嵌缓存。

V8维护一个对象类型的缓存;这些对象在最近的方法调用中被当做传参,然后V8根据这个缓存信息来推断将来什么样类型的对象会再次被当成传参。如果V8能够准确推断出接下来被传入的对象类型,那么它就能绕开获取对象属性的计算步骤,而只是使用先前查找该对象的隐藏类时所存储的信息。

5. 内存管理

1. Zone对象

Zone对象首先自己申请一块内存,然后管理和分配一些小内存。当一块小内存被分配之后,不能够被Zone回收,只能一次性回收Zone分配的所有小块内存。

2. 堆

V8使用堆来管理JS使用的数据,以及生成的代码、哈希表等。

为了实现垃圾回收,V8将堆分为三个部分,第一个是年轻分代,第二个是年老分代,第三个是大对象空间。

对于年轻分代,主要是为新创建的对象分配内存空间,因为年轻分代总的对象比较轻易被要求回收,为了方便垃圾回收,可以使用复制方式,将年轻分代分为两半,一半用来分配,另外一半在回收的时候负责将之前还需要保留的对象复制过来。

对于年轻分代,经常需要进行垃圾回收。
对于年老分代,主要是根据需要将年老的对象、指针、代码等数据使用的内存较少地做垃圾回收。
对于大对象空间,主要是用来为那些需要使用较多内存的大对象分配内存。

3. 垃圾回收算法

V8使用的是“标记-清除”垃圾回收算法,该算法比“引用计数”垃圾回收算法更好,可以避免循环引用导致无法回收垃圾的情况。

“标记-清除”算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

标记阶段会阻止 JavaScript 的运行。为了控制垃圾回收的成本,并且使 JavaScript 的执行更加稳定,V8 使用增量标记:与遍历全部堆去标记每一个可能的对象的不同,取而代之的是它只遍历部分堆,然后就恢复正常执行。下一次垃圾回收就会从上一次遍历停下来的地方开始,这就使得每一次正常执行之间的停顿都非常短。就像前面说的,清理的操作是由独立的线程的进行的。

6. 快照机制

快照机制是将内置的对象和函数加载之后的内存保存并序列化。序列化之后的结果很容易被反序列化,经过快照机制的启动时间,可以大幅缩减。

7. 题外话:JavaScriptCore

JavaScriptCore和V8最大差别在于,把源代码转变成抽象语法树(AST)之后,JavaScriptCore会将AST转化为字节码,该字节码是运行在解释器上面的,并由JIT不断的改进优化。而V8则通过全代码生成器(full code generator)把抽象语法树直接生成本地代码,为了性能考虑,还会通过数据分析器(Profiler)来采集一些信息,来帮助决策哪些本地代码需要优化,以生成效率更高的本地代码,这是一个逐步改进的过程。

参考

书籍

《WebKit 技术内幕》

链接

V8基础学习一:从编译流程学习V8优化机制
[译] JavaScript 如何工作:在 V8 引擎里 5 个优化代码的技巧
Google V8 引擎工作原理(翻译)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions