-
Notifications
You must be signed in to change notification settings - Fork 20
Description
概述
上面的图片摘自《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 引擎工作原理(翻译)

