📜  V8 如何编译 JavaScript 代码?

📅  最后修改于: 2022-05-13 01:56:45.663000             🧑  作者: Mango

V8 如何编译 JavaScript 代码?

V8 是 Google Chrome 和 Node.js 使用的高性能、开源 JavaScript 和 WebAssembly 引擎。在本文中,我们将了解 V8 架构背后发生的事情。

处理代码基本上涉及三个步骤:

  • 解析代码
  • 编译代码
  • 执行代码

现在让我们深入了解每个阶段。

1.解析阶段:在解析阶段,代码被分解成各自的标记。

例子:

const sum = 5 + 7

这里const是一个token,sum是一个token,5是一个token,'+'是一个token,7是一个token。将代码分解为标记后,将其提供给语法解析器,该语法解析器将代码转换为抽象语法树 (AST)。

下面是为上述示例生成的 AST:

抽象语法树

抽象语法树

2.编译阶段:编译是将人类可读的代码转换为机器代码的过程。有两种编译代码的方法:

  • 使用解释器:解释器逐行扫描代码并将其转换为字节码。示例: Python
  • 使用编译器:编译器扫描整个文档并将其编译成高度优化的字节码。示例: Java

与其他语言不同,V8 引擎同时使用编译器和解释器,并遵循即时 (JIT) 编译以提高性能。

即时(JIT)编译: V8 引擎最初使用解释器来解释代码。在进一步的执行中,V8 引擎会发现频繁执行的函数、频繁使用的变量等模式,并对其进行编译以提高性能。假设性能下降或传递给函数的参数改变了它们的类型,那么 V8 只需反编译编译的代码并回退到解释器。

示例:如果编译器编译一个函数,假设从 API 调用获取的数据是 String 类型,那么当接收到的数据是 object 类型时,代码会失败。在这种情况下,编译器反编译代码,回退到解释器,并更新反馈。 V8引擎使用Ignition解释器,将抽象语法树作为输入,给出字节码作为输出,进一步进入执行阶段。在解释代码时,编译器会尝试与解释器对话以优化代码。 V8 引擎使用Turbofan编译器,它将来自解释器的字节码和反馈(来自解释器)作为输入,并给出优化后的机器码作为输出。

3、执行阶段:字节码通过V8引擎运行环境的Memory heap和Call Stack来执行。内存堆是为所有变量和函数分配内存的地方。调用堆栈是每个单独的函数在调用时被压入堆栈并在执行后弹出的地方。当解释器解释代码时,使用对象结构,其中键是字节码,值是处理相应字节码的函数。 V8 引擎在内存中以列表的形式对值进行排序,将其保存到 Map 中,从而节省大量内存。

例子:

let Person = {name: "GeeksforGeeks"}
Person.age = 20;

在上面的示例中,地图包含 Person 对象,该对象具有属性名称。第二行创建一个具有属性 age 的新对象并将其链接回 Person 对象。上述方法的问题是搜索链表需要线性时间。为了解决这个问题,V8 为我们提供了 Inline Cache(IC)。

内联缓存:内联缓存是一种数据结构,用于跟踪对象属性的地址,从而减少查找时间。它通过维护一个反馈向量来跟踪函数中的所有 LOAD、STORE 和 CALL 事件。反馈向量只是一个数组,用于跟踪特定函数的所有内联缓存。

例子 :

const sum = (a, b) => {
    return a+b;
}

对于上面的例子,IC是:

[{ slot: 0, icType: LOAD, value: UNINIT}]

这里,该函数有一个类型为 LOAD 且值为 UNINIT 的 IC,这意味着该函数尚未初始化。

调用函数时:

sum(5, 10)
sum(5, "GeeksForGeeks")

在第一次调用时,IC 更改为:

[{ slot: 0, icType: LOAD, value: MONO(I) }]

此处代码以某种方式解释,其中传递的参数仅为整数类型。即该函数仅适用于整数值。

在第二次调用中,IC 更改为:

[{ slot: 0, icType: LOAD, value: POLY[I,S] }]

这里代码以某种方式解释,其中传递的参数可以是整数类型或字符串。即该函数将适用于整数和字符串。因此,如果不修改接收到的参数类型,则函数的运行时间会更快。内联缓存跟踪它们的使用频率并向 Turbofan 编译器提供必要的反馈。编译器从解释器获取字节码和类型反馈,并尝试优化代码并生成新的字节码。假设编译器编译一个函数,假设从 API 调用获取的数据是 String 类型,当接收到的数据是 object 类型时代码会失败。在这种情况下,编译器反编译代码,回退到解释器,并更新反馈。

JavaScript 代码的编译和执行是齐头并进的。

下面是 JavaScript 代码编译的图解表示。

V8 引擎尝试通过清除未使用的函数、清除超时、清除间隔等来释放内存堆。

现在,让我们了解垃圾收集的过程。

垃圾收集:这是编程的一个重要方面,垃圾收集器使用的技术改善了延迟、页面加载、暂停时间等。V8 引擎提供了 Orinoco 垃圾收集器,内部使用标记和扫描算法从内存堆中释放空间。

Orinoco 垃圾收集器使用三种方式收集垃圾:

  • 并行:在并行收集中,JavaScript 主线程并行使用少量辅助线程的帮助来清除垃圾,因此,主执行只停止了一段时间。
  • 增量:在增量收集中,主 JavaScript 线程轮流收集垃圾,即以增量方式。这种类型的集合用于进一步减少主线程的延迟。示例:JavaScript 线程首先收集垃圾一段时间,然后切换到主执行一段时间,然后切换回垃圾收集。这个过程一直持续到整个垃圾被收集。
  • 并发:在并发收集中,主 JavaScript 线程不受干扰,整个 Garbage 由后台的辅助线程收集。