JavaScript代码是如何被V8编译的
V8是一个高性能、开源的JavaScript和WebAssembly引擎,被Google Chrome和Node.js使用。在本文中,我们将深入了解V8架构背后发生了什么。
代码处理过程中涉及基本上三个步骤:
- 解析代码
- 编译代码
- 执行代码
现在让我们深入了解每个阶段。
1. 解析阶段: 在解析阶段,将代码分解成各个标记。
示例:
const sum = 5 + 7
在这里,const是一个标记,sum是一个标记,5是一个标记,’+’是一个标记,7是一个标记。在代码被分解为标记之后,它被传递给语法解析器,将代码转换为抽象语法树(AST)。
下面是上述示例生成的AST:

2. 编译阶段: 编译是将可读的人类代码转换成机器代码的过程。有两种编译代码的方式:
与其他语言不同,V8引擎同时使用编译器和解释器,并遵循即时编译(JIT)编译以提高性能。
即时编译(JIT): V8引擎首先使用解释器解释代码。在后续执行中,V8引擎找到频繁执行的函数、频繁使用的变量等模式,并将其编译以提高性能。如果性能下降或传递给函数的参数改变其类型,则V8简单地对编译代码进行反编译,并回退到解释器。
示例: 如果编译器编译一个函数时假设从API调用中获取的数据是字符串类型,而实际接收到的数据是对象类型,那么代码将失败。在这种情况下,编译器对代码进行反编译,回退到解释器,并更新反馈。V8引擎使用 Ignition 解释器,以抽象语法树作为输入,并将字节码作为输出,进而进入执行阶段。在解释代码时,编译器尝试与解释器进行通信以优化代码。V8引擎使用 Turbofan 编译器,以解释器的字节码和反馈(来自解释器)作为输入,并生成优化的机器码作为输出。
3. 执行阶段: 使用V8引擎的运行时环境的 内存堆和调用栈 执行字节码。 内存堆 是分配所有变量和函数内存的地方。 调用栈 是将每个单独的函数在调用时推送到栈上并在执行后弹出的地方。当解释器解释代码时,使用对象结构,其中键是字节码,值是处理相应字节码的函数。V8引擎将值按照列表的形式在内存中保存,并将其保存在Map中,从而节省了大量内存。
示例:
let Person = {name: "GeeksforGeeks"}
Person.age = 20;
在上面的示例中,一个映射(map)包含有属性名字的人(Person)对象。第二行创建了一个具有属性年龄,并将其链接回 Person 对象的新对象。上述方法的问题在于,通过链表进行搜索需要线性时间。为了解决这个问题,V8 提供了内联缓存(Inline Cache)。
内联缓存: 内联缓存是一种用于跟踪对象属性地址的数据结构,从而减少查找时间。它通过维护 反馈向量(Feedback Vector) 来跟踪函数中的所有 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调用中获取的数据是字符串类型,当接收到的数据是对象类型时,代码将失败。在这种情况下,编译器会反编译代码,回退到解释器,并更新反馈。
Javascript代码的编译和执行是相辅相成的。
以下是Javascript代码编译的示意图:

V8引擎尝试释放内存堆,清除未使用的函数、清除超时、清除间隔等。
现在,让我们了解垃圾收集的过程。
垃圾收集: 它是编程的重要方面,垃圾收集器使用的技术改进了延迟、页面加载、暂停时间等。V8引擎通过使用内部的奥里诺科垃圾收集算法从内存堆中释放空间。
奥里诺科垃圾收集器使用三种方式来收集垃圾:
- 并行: 在并行收集中,主JavaScript线程使用几个辅助线程同时清除垃圾,因此主执行仅暂停片刻。
- 渐进: 在渐进收集中,主JavaScript线程轮流收集垃圾,即逐步收集。这种收集方式用于进一步减少主线程的延迟。例如:JavaScript线程首先收集一段时间的垃圾,然后切换到主执行一段时间,然后再切换回垃圾收集。此过程一直进行,直到收集完整的垃圾。
- 并发: 在并发收集中,主JavaScript线程不受干扰,所有垃圾都由辅助线程在后台收集。
极客教程