Skip to content

V8 原理之 JS 执行

V8 引擎是什么

  • Google V8 引擎是用 C++ 编写的开源高性能 JavaScriptWebAssembly 引擎。已被用于 Chrome 和 Node.js 等。
  • V8 解析/编译/执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。
  • V8 属于 JIT 编译器
    • 在运行C、C++以及Java等程序之前,需要进行编译,不能直接执行源码;
    • 但对于 JavaScript 来说,可直接执行源码,它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称 JIT。
  • V8 率先引入了即时编译(JIT)的双轮驱动的设计(混合使用 编译器解释器 的技术)

V8 核心模块

模块定义及工作流程

四个核心模块:解析器,解释器,编译器,垃圾回收

简单来说,Parser 将 Javascript 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)

browser-v8

  • Parser(解析器)
    • 负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
  • Ignition:Interpreter(解释器)
    • 负责将 AST 转换为 Bytecode,解释执行 Bytecode
    • 同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;
    • 解释器执行时涉及四个模块:内存中的字节码、寄存器、栈、堆
    • node --print-bytecode test.js
  • TurboFan:Compiler(编译器)
    • 利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的 Machine Code (汇编代码)
    • node --print-code --print-opt-code test.js
  • Orinoco:Garbage Collector(垃圾回收)
    • 负责将程序不再需要的内存空间回收。

优化与反优化

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用1次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan不会进行优化编译,因为它需要Ignition收集函数执行时的类型信息。这就要求函数至少需要执行1次,TurboFan才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为 热点函数,且 Ignition 收集的类型信息证明可以进行优化编译 的话,这时TurboFan则会将Bytecode编译为 Optimized Machine Code,以提高代码的执行性能。
  • 图片中的 红线是逆向的,Optimized Machine Code 会被还原为 Bytecode,这个过程叫 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。
/*
 * 生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的;
 * 于是需要进行 Deoptimization。
 */
function add(x, y) {
  return x + y;
}

add(3, 5);
add('3', '5');

执行一段 JavaScript 代码

  • V8 本质上是一个 虚拟机,模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。
  • 计算机执行一段高级语言通常有两种手段
    • 一种是将高级代码转换为二进制代码,再让计算机去执行,即 编译执行
    • 另一种是在计算机安装一个解释器,并由解释器来解释执行,即 解释执行
  • 解释执行启动快,执行时慢;而编译执行启动慢,但是执行快
  • V8 率先引入了 即时编译(JIT)的双轮驱动的设计(混合使用编译器和解释器的技术),极大提升了 JavaScript 的执行速度。

执行 JavaScript 流程图

x

  • 初始化基础环境;
  • 解析源码生成 AST 和作用域;
  • 依据 AST 和作用域生成字节码;
  • 解释执行字节码;
  • 监听热点代码;
  • 优化热点代码为二进制的机器代码;
  • 反优化生成的二进制机器代码

机器码、字节码

函数

  • 如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为 一等公民
  • JavaScript 中函数是一等公民。

闭包

  • JavaScript 允许在函数内部定义新的函数;
  • 可以在内部函数中访问父函数中定义的变量;
  • 因为 JavaScript 中的函数是 一等公民,所以函数可以作为另外一个函数的返回值。

惰性解析

  • 惰性解析(懒解析) 是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
  • 为啥这样设计
    • 首先,当一个页面的 JavaScript 代码很大,如果要将所有的代码一次性解析编译完成,那么会大大 增加用户的等待时间
    • 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直 占用内存
  • 问题:闭包 引用的外部变量不能随着函数执行上下文销毁。

预解析器

当解析顶层代码的时候,遇到了一个函数,那么 预解析器 并不会直接跳过该函数,而是对该函数做一次快速的预解析:

  • 判断当前函数是不是 存在语法错误,若存在,向 V8 抛出语法错误;
  • 检查函数内部是否引用了 外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就 解决了闭包所带来的问题

堆栈内存模型

栈空间

  • 栈空间用来管理 JavaScript 函数调用。
  • 在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等。
  • 栈空间的最大的特点是 空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率高。

堆空间

  • 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据。
  • JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。

References

问题:浏览器 V8 是如何工作的?