JavaScript V8 引擎

2025 年 3 月 3 日 | 阅读 10 分钟

2008 年,Google 改进了 V8 引擎,以提高 Google Chrome(该公司的网络浏览器)中 JavaScript 的速度。以前,人们认为大型任务需要 JavaScript 运行缓慢且效率低下,尤其是与其他 编程语言(如 JavaC)相比。在 8 版本之前,JavaScript 引擎会解释 JavaScript 代码,而不会进行任何重大的优化,这有时会导致网络应用程序运行缓慢。

通过 JavaScript V8,Google 旨在打破速度障碍,使该语言足够快速,可以开发可靠、交互式的网络应用程序。对即时响应时间要求极高的尖端网络应用(如 Google Maps、Gmail 等)的出现,进一步增加了对这种效率的需求。

V8 引擎架构

随着 V8 的发布,JavaScript 的性能发生了革命性的变化,树立了一个新的标杆,促使其他浏览器公司(包括 Apple 的 JavaScriptCore 和 Mozilla 的 SpiderMonkey)重新考虑它们的引擎设计。

1. 解释器(Ignition)

V8 以字节码解释器 Ignition 开始 JavaScript 代码的执行过程。以下是其工作原理:

JavaScript 解析: 当 V8 遇到 JavaScript 时,它首先将源代码解析成一个抽象语法树 (AST)。此过程有助于引擎理解代码的结构。

字节码生成: Ignition 生成的是字节码,而不是直接将代码转换为机器码。JavaScript 源代码以字节码的形式表示,这是一种平台无关的中间格式,比机器码生成速度更快。这会导致快速但不太完整的初始执行。

2. 即时 (JIT) 编译

即时 (JIT) 编译机制是 V8 最重要的特性之一,旨在最大化性能。V8 使用两级 JIT 过程将经常使用的代码段转换为高效的机器码。

基线 JIT (TurboFan): 在字节码执行期间,V8 会检测到“热”代码或经常执行的代码。这通常通过一种称为 JIT 编译的技术来完成,将其转换为机器码,效率更高。V8 使用其 TurboFan JIT 编译器来完成此任务。

热代码分析: 引擎会跟踪特定函数或循环的运行频率。当检测到某段代码被执行一次以上时,它会启动 JIT 编译过程来优化该代码段。

机器码生成: 在 TurboFan 将字节码转换为机器码后,便会生成特定于宿主 CPU 架构(x86、ARM 等)的机器码。下次执行代码时,V8 直接使用机器码,从而避免了较慢的解释过程。

3. 垃圾回收(内存管理)

由于 V8 拥有复杂的垃圾回收器,可以自动回收未被引用的对象所占用的内存,因此 JavaScript 开发人员无需手动管理内存。V8 的内存管理方法称为分代垃圾回收。

新生代(New Space): 这是为新创建的对象分配的区域。由于 V8 假定大多数对象都是短暂的,因此该区域会经常被垃圾回收器清理。

老生代(Old Space): 在新生代中经过多次垃圾回收仍存活的对象会被移至老生代,这里的清理频率较低。

垃圾回收方法

标记-清除 (Mark-and-Sweep): V8 查找仍在使用中的对象并将其标记为“活动”,然后“清除”未被标记的对象。

增量标记 (Incremental Marking): 为了避免在垃圾回收期间可能发生的长时间应用程序执行暂停,V8 通过将任务分解成更小的部分来实现增量垃圾回收。

压缩 (Compaction): 为了防止内存碎片化并确保更有效的内存使用,V8 会通过将活动对象彼此靠近来定期压缩内存。

4. 内联缓存

内联缓存是 V8 使用的一种速度优化技术。当一个函数被反复调用时,V8 会记住函数前一次调用提供的参数和其他详细信息。这有助于 V8 优化后续的调用。

当访问 JavaScript 对象的属性时,访问模式是相似的。例如,V8 可能会直接获取属性,而无需进行多次查找。因此,代码运行速度更快,因为引擎不必为每次调用进行不必要的查找或检查。

5. 隐藏类和对象布局

由于 JavaScript 对象非常动态,可以在运行时更改其属性和方法以及整体结构。这种灵活性给引擎优化带来了挑战。V8 通过引入隐藏类的概念来解决此问题,隐藏类在功能上类似于静态类型语言中的类,但在应用程序运行时动态生成。

隐藏类: V8 在创建对象时,会根据对象的初始结构为其分配一个隐藏类。每次添加或删除属性时,引擎都会生成新的隐藏类。这些隐藏类使 V8 能够优化属性访问,因为它们实现了有序的内存布局,从而加快了读写速度。

过渡链: 每次对象的结构发生变化(例如,通过添加新属性)时,V8 都会生成一个新的隐藏类,并将其链接到现有类。这有助于引擎在不影响属性访问速度的情况下有效处理更改。

6. 执行管道

这是 V8 执行流程的概述:

JavaScript 解析: 通过解析源代码创建抽象语法树 (AST)。

字节码生成: 解释器 Ignition 通过组合 AST 来创建字节码。

字节码执行: V8 在运行时执行字节码,同时密切关注性能。

JIT 编译: TurboFan 检测热代码并将其编译为机器码。

垃圾回收: 在执行期间管理内存,以释放不再需要的对象占用的空间。

去优化(如果需要): 如果在优化期间所做的假设被证明是错误的,V8 会回退到使用字节码的较慢执行模式。

设计目标和原则

1. 速度和效率

V8 的主要设计目标是速度。在 V8 出现之前,JavaScript 引擎速度很慢,因为它们不会将 JavaScript 代码编译成机器码,而是直接进行解释。通过使用更复杂的执行机制,包括即时 (JIT) 编译和优化技术,V8 被开发出来以打破这种性能瓶颈。为实现这一目标采用了多种策略:

  • 更快的浏览器响应时间和页面加载速度,从而提高了网络应用程序的流畅性和交互性。
  • 在 Node.Js 等服务器端环境中提供出色的性能,使可伸缩的应用程序能够同时处理大量用户和任务。

2. 内存准确性

对于 V8 引擎来说,高效的内存管理仍然是一个重要目标。由于 JavaScript 对象在执行过程中动态创建和销毁,因此不当的内存管理可能导致内存泄漏、碎片化和过高的内存消耗。为了缓解这些问题,V8 采用了先进的垃圾回收技术。

  • 优化内存消耗是浏览器等环境的关键功能,因为浏览器需要同时加载许多网页。
  • V8 能够处理大型应用程序和数据密集型过程,尤其是在 Node.Js 环境中,而不会导致过高的内存使用量或因垃圾回收暂停而导致的性能下降。

3. 可移植性和跨平台兼容性

由于其可移植性,V8 可以在各种硬件架构和操作系统上运行。这尤其重要,因为 JavaScript 代码可以在各种系统上执行,包括服务器、移动设备和台式计算机。

  • V8 适用于服务器端 JavaScript 环境(如 Node.Js)、嵌入式系统和移动浏览器,因为它可以在多种环境中部署。
  • JavaScript 代码的编写者只需编写一次代码,就可以在不同平台上运行,而无需进行特定于硬件或操作系统的修改。

4. 安全性

由于网络浏览器经常成为攻击目标,因此安全性是 V8 的首要设计考虑因素。为了在保持性能的同时保护用户,V8 具有多层安全保护。

  • V8 网络浏览器(如 Google Chrome)更能抵御常见的网络威胁,例如代码注入攻击、跨站点脚本 (XSS) 和跨站点请求伪造 (CSRF)。
  • 由于 JavaScript 引擎中的安全漏洞,基于 V8 的应用程序(包括使用 Node.Js 创建的应用程序)泄露敏感数据的风险较低。

5. ECMAScript 标准兼容性

ECMAScript (ES) 标准定义了新的语法、特性和功能,其更新导致 JavaScript 不断发展。V8 的设计目标是具有极高的性能和内存效率,同时仍兼容最新的 ECMAScript 版本。由于这种兼容性,开发人员无需等待引擎支持即可使用最新的 JavaScript 功能。

  • 开发人员可以放心地使用 V8,因为 V8 将支持最新的 JavaScript 功能,而不会牺牲速度。
  • 通过使用现代网络应用程序可用的新语法和功能,代码可以变得更纯净、更高效、更易于维护。

6. 开发人员体验

通过可预测的 JavaScript 性能以及性能监控、分析和调试工具来改进整体开发人员体验,是 V8 设计的另一个基本指导原则。

  • 借助内置工具,开发人员还可以创建高性能的 JavaScript 代码,并快速进行调试和优化。
  • 通过使用 V8 的工具,开发人员可以更快地构建高性能、无错误的应用程序。

V8 引擎在 Node.js 中的作用

V8 引擎为 Node.js 的服务器端 JavaScript 代码执行提供了基础。Node.js 是一个运行时环境,它使得 JavaScript 能够在浏览器之外运行。Node.js 扩展了 JavaScript 的能力,使其能够处理服务器端逻辑、文件访问、数据库交互和 HTTP 请求。JavaScript 通常用作客户端语言来创建动态网页。

1. JavaScript 的执行

从根本上说,V8 在 Node.js 中的主要职责是运行 JavaScript 代码。V8 引擎读取 JavaScript 文件并将其转换为机器码,然后启动 Node.Js 应用程序。此过程包含多个步骤:

解析: V8 最初将 JavaScript 代码解析成抽象语法树 (AST)。这有助于引擎理解代码的结构。

Ignition(解释器): V8 中的 Ignition 解释器将 AST 转换为字节码,这是一种更简单但仍然平台无关的代码表示。虽然运行速度比机器码快,但字节码的效率不如机器码。

TurboFan(JIT 编译器): 在字节码执行过程中,V8 的 TurboFan 编译器会检测到“热”代码,并将其转换为机器码。机器码针对宿主 CPU 进行了优化,并且运行速度比字节码快得多,从而提高了 Node.js 应用程序的性能。

2. 即时编译 (JIT)

即时 (JIT) 编译是 V8 用来最大化 JavaScript 执行效率的主要方法。JIT 编译过程会分析代码的执行情况,以检测经常执行(或“热”)的代码段。

热代码: V8 将某些函数或操作归类为“热”,并在检测到它们经常使用时,使用 TurboFan 编译器将它们编译成优化的机器码。由于无需多次解释相同的字节码,因此性能得到了提高。

优化: 在 JIT 编译过程中,V8 使用多种优化技术,包括内联缓存、隐藏类和推测性优化(即,在假设某个变量始终为特定类型的情况下进行优化)。这些优化的目的是加快服务器环境中 JavaScript 的速度。

去优化: 如果 V8 发现其关于代码的假设(包括变量的数据类型)是错误的,它能够将优化后的代码恢复为较慢的、更通用的版本。我们将此过程称为去优化。

3. 内存组织和垃圾回收

垃圾回收允许 JavaScript 自动管理内存。V8 包含一个复杂的垃圾回收器,这对 Node.js 至关重要。这对于需要长时间处理大量客户端请求的服务器进程尤其有用。不当的内存管理导致的内存泄漏最终可能导致服务器崩溃或运行缓慢。

新生代: 新创建的对象被分配到这个区域。这些对象通常会被垃圾回收,并且被认为寿命较短。

老生代: 经过多次垃圾回收仍存活的对象会被移至此区域。由于该区域的对象寿命较长,因此清理频率较低。

4. 性能增强

隐藏类:为了最大化属性访问。V8 为对象提供隐藏类。由于 JavaScript 对象是动态的,因此在代码执行过程中其结构可以改变。V8 使用隐藏类更有效地组织对象,从而加快了属性访问和修改的速度。

内联缓存: V8 采用内联缓存来最大化重复任务的效率。为了避免不必要的查找,如果属性访问模式保持不变,V8 会缓存访问属性所需的信息。

高效的异步执行: V8 经过调优,能够处理异步、非阻塞 I/O 操作,这是 Node.js 的基础。在处理输入/输出 (I/O) 绑定的任务(如文件访问、网络调用等)时,V8 不会暂停其他操作来等待 I/O 完成。

5. Node.js 集成

虽然 V8 引擎是 Node.js 的关键组成部分,但重要的是要认识到 Node.js 不仅仅是 V8。Node.js 提供了大量库和 API,扩展了 JavaScript 的服务器端功能。这些功能包括:

HTTP 模块通过允许 JavaScript 处理 HTTP 请求,简化了 Web 服务器的构建。

流和缓冲区: 在处理大量数据(如文件或 HTTP 响应)时,Node.js 会有效地利用流和缓冲区。

事件循环: 为了有效地管理异步过程,Node.js 的事件循环与 V8 协同工作。在等待 I/O 操作(如文件读取或网络请求)完成时,事件循环会安排回调供 V8 引擎执行。