V8 原理之 JS 执行
V8 引擎是什么¶
- Google V8 引擎是用 C++ 编写的开源高性能
JavaScript
和WebAssembly
引擎。已被用于 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(实际上是汇编代码)
。
- 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 流程图¶
- 初始化基础环境;
- 解析源码生成 AST 和作用域;
- 依据 AST 和作用域生成字节码;
- 解释执行字节码;
- 监听热点代码;
- 优化热点代码为二进制的机器代码;
- 反优化生成的二进制机器代码
机器码、字节码¶
函数¶
- 如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为 一等公民。
- JavaScript 中函数是一等公民。
闭包¶
- JavaScript 允许在函数内部定义新的函数;
- 可以在内部函数中访问父函数中定义的变量;
- 因为 JavaScript 中的函数是 一等公民,所以函数可以作为另外一个函数的返回值。
惰性解析¶
- 惰性解析(懒解析) 是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
- 为啥这样设计
- 首先,当一个页面的 JavaScript 代码很大,如果要将所有的代码一次性解析编译完成,那么会大大 增加用户的等待时间;
- 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直 占用内存。
- 问题:闭包 引用的外部变量不能随着函数执行上下文销毁。
预解析器¶
当解析顶层代码的时候,遇到了一个函数,那么 预解析器 并不会直接跳过该函数,而是对该函数做一次快速的预解析:
- 判断当前函数是不是 存在语法错误,若存在,向 V8 抛出语法错误;
- 检查函数内部是否引用了 外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就 解决了闭包所带来的问题。
堆栈内存模型¶
栈空间¶
- 栈空间用来管理 JavaScript 函数调用。
- 在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等。
- 栈空间的最大的特点是 空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率高。
堆空间¶
- 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据。
- JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。