了解 V8 引擎


作者:Seiya

时间:2019年06月01日


前言


V8 是 Google 开源的一个高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写,最开始是由一些语言方面的专家设计出来的,后被 Google 收购。

V8 编译并执行 JavaScript 源代码,处理对象的内存分配,同时回收不再使用的对象的内存,精确的垃圾收集器是V8性能的关键之一。它支持众多的操作系统以及众多的硬件架构,它将主流软硬件平台一网打尽,由于它是一个开源项目,开发者可以自由使用它的能力,一个例子就是目前的 NodeJs 项目。


WebAssembly

一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。

参考资料: WebAssembly - MDN - Mozilla



关键概念


要了解 V8 引擎的内部工作机制,有必要先了解 V8 引擎的一些相关概念,我们可以根据官方文档上的介绍大概理解 v8 引擎的工作方式。下面来看看这些概念分别表示什么意思:

  • Isolate

    表示 V8 引擎的一个实例,包含相关状态信息、堆等;

  • Local handle

    表示本地句柄,它是一个指向对象的指针。所有的 V8 对象都是通过句柄进行访问,由于 V8 垃圾收集器的特殊工作方式,它是必须的;

  • Handle scope

    一个包含任意数量的句柄容器,我们可以进行一次性的删除句柄操作,避免重复调用;

  • Context

    表示执行上下文,它是一种执行环境。允许 JavaScript 应用单独的、不相关的 运行在 V8 引擎的实例中。而要执行任何 JavaScript 代码,我们必须明确指定其运行的上下文;



详细介绍

现在我们已经熟悉 V8 引擎的一些关键概念(如句柄,范围和上下文),现在进一步讨论这些概念,并介绍一些其他概念:


1. Handles and garbage collection:句柄和垃圾回收

  • 句柄:

    提供了 javascript 对象在堆中位置的引用;

  • 垃圾收集器:

    回收不可访问对象使用的内存;

    • 在垃圾回收过程中,垃圾收集器经常移动堆中对象的位置,当垃圾收集器移动一个对象时,它也会更新所有指向对象的句柄,使其指向对象的新位置;

    • 如果一个对象在 JavaScript 代码中不可访问,且没有任何句柄指向这个对象,则该对象会被视为垃圾。垃圾收集器会不时删除所有被认为是垃圾的对象。


  • 下面介绍两种类型的句柄:

    其他的句柄很少被提到,所以只介绍这两种。

    • Local handle:本地句柄

      本地句柄保存在堆栈(stack)上,并在其析构函数被调用时被删除。这些句柄的生命周期由句柄作用域(handle scope)决定,该作用域通常在函数调用开始时创建;

      删除句柄作用域时,垃圾收集器可以自由释放句柄作用域中以前引用的对象,前提是这些对象不再可以从JavaScript或其他句柄访问;

      析构函数

      析构函数 (destructor) 与构造函数相反,当对象结束其生命周期,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作。

    • Persistent handle:持久句柄

      持久句柄并不保存在堆栈中,并且仅当你专门删除它们时才会被删除。

      像本地句柄一样,持久句柄也保存指向堆分配对象的引用。如果你需要在多个函数中访问同一个对象,或者句柄的生命周期和C++作用域并不相同时,你可以使用持久句柄;


注意:

每创建一个对象都会创建一个句柄,这可能会导致出现大量的句柄!这时句柄作用域就非常有用了。句柄作用域的析构函数被调用时会从堆栈中删除该作用域内所有的句柄。这会使所有被删除句柄所指向的对象被垃圾回收器标记为从堆中删除。


2. Context:执行环境

前面提到过,要执行任何 JavaScript 代码,必须显式的指定其运行的上下文。V8 引擎使用上下文的初衷是,使每个窗口和浏览器的 iframe 可以有它们自己的 JavaScript 环境。

从CPU时间和内存的角度看来,创建一个新的执行环境并建立必须的对象,可能是非常昂贵的开销。

  • 必要性:

    因为 JavaScript 提供了一套内置的实用函数和对象,并且它们可以被 JavaScript 代码修改。而当两个完全不相干的 JavaScript 函数都以同样的方式改变了全局对象,则很可能会产生意外结果。例如一个页面 JavaScript 代码修改内置对象方法 toString,不应该影响到另外页面。

  • 优化:

    V8引擎中广泛存在的缓存特性可以保证,第一次创建上下文是比较昂贵的,但其后创建上下文的花费会少很多。

    因为第一个上下文(context)需求创建内置对象并解析内置的 JavaScript 代码(built-in JavaScript code),但随后创建的上下文只需要创建自己的内置对象即可。

  • 快照机制(Snapshot):

    快照机制就是将这些内置的 JavaScript 对象和函数加载之后的内存保存并序列化。 通过构建参数 snapshot=yes 来激活,默认处于激活状态。

    这样第一次创建上下文所花的时间也将会被很大程度的优化。因为快照功能包含一个经过序列化的堆,其中已经包括编译好的 JavaScript 内置对象和函数的代码。与垃圾收集机制一样,V8引擎中广泛存在的缓存也是V8引擎高性能的关键。


3. Templates:模板

在上下文中(Context),模板是JavaScript函数和对象的框架。您可以使用模板将C++函数和数据结构包装在JavaScript对象中,使他们可以通过JavaScript脚本来操作。

举个例子:Chrome使用模板将 C++ DOM 节点包装成 JavaScript 对象,并将其安装在全局命名空间中。这样我们就可以通过 JavaScript 代码操作 DOM 对象。下面我们介绍两种类型的模板:


  • 函数模板

    函数模板是一个函数的框架。你可以在想要生成 JavaScript 函数的上下文中调用模板的”GetFunction”方法来创建一个该模版的 JavaScript 实例。您还可以将一个c++回调函数和一个函数模版相关联,这个回调函数将在 JavaScript 函数执行时被调用。

  • 对象模板

    每个函数模板都有一个与之关联的对象模板。它用来配置使用这个函数作为构造函数所创建的对象。对于对象模板,你可以将两种C++回调函数与之关联:

    • 存取回调(accessor callbacks):在对象的指定属性被脚本访问时调用;

    • 拦截回调(interceptor callbacks):在对象的任意属性被脚本访问时调用;


4. Accessors:存取器

存取器(accessors)是一个C++回调函数,在JavaScript脚本存取对象属性时计算并返回该属性的值。存取器可以通过对象模板的SetAccessor方法来设置。此方法接受一个属性名称和两个回调函数分别用于读取和写入属性的值。


一个存取器的复杂程度取决于它们操作的数据类型:

  • 存取静态全局变量

  • 存取动态变量


5. Interceptors:拦截器

我们还可以指定一个在脚本访问对象的任意属性时都会触发的回调,这些回调被称为拦截器(interceptors)。为了提高效率,V8中有两种类型的拦截器:

  • 命名属性拦截器 :使用属性名访问属性时被调用。

    例如,在浏览器环境中,使用document.theFormName.elementName。

  • 索引属性拦截器 :使用索引访问属性时被调用。

    例如,在浏览器环境中,使用document.forms.elements[0]


和存取器相同,只要属性被访问,指定的回调都会被调用。存取器和拦截器之间的不同是:拦截器可以处理所有属性,但存取器只与一个特定的属性相关联。


6. Security model:安全模型

在V8中“源”被定义为一个上下文。默认情况下,不能在一个作用域中访问其他作用域。要访问和当前作用域不同的作用域,你需要使用安全令牌(security token)或安全回调(security callbacks)。

当试图将访问一个全局变量时的V8引擎安全系统首先检查被访问全局对象的安全令牌和访问代码的安全令牌。如果令牌匹配则授予访问权限。如果不匹配V8引擎会执行一个回调,用以判定是否应该允许访问。

安全令牌

一个安全令牌可以是任何值,但通常是一个符号或一个唯一的字符串。当定义上下文时,您可以通过 SetSecurityToken 指定安全令牌。如果你不指定安全令牌 V8 引擎将自动为新建的上下文生成令牌。

同源规则

阻止从一个“源”中加载的文件或脚本获取或设置不同“源”的文档属性。这里的“源”包括 域名协议端口,所有这三个必须匹配,才被视为同源。如果没有这种保护,恶意网页可能会影响其他正常的网页。

最后更新时间: 2019-7-7 21:55:38