V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行。
上图清晰版
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码。
JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。
1000100111011000 #机器指令 mov ax,bx #汇编指令
资料拓展: 汇编语言入门教程【阮一峰】 | 理解 V8 的字节码「译」
Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。
和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。
资料拓展:v8 logo | V8 (JavaScript engine) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly
d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。
方法一:自行下载编译
方法二:使用编译好的 d8 工具
// 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开) V8 version 8.4.109 d8> 1 + 2 3 d8> 2 + '4' "24" d8> console.log(23) 23 undefined d8> var a = 1 undefined d8> a + 2 3 d8> this [object global] d8>
本文后续用于 demo 演示时的文件目录结构:
V8: # d8可执行文件 d8 icudtl.dat libc++.dylib libchrome_zlib.dylib libicui18n.dylib libicuuc.dylib libv8.dylib libv8_debug_helper.dylib libv8_for_testing.dylib libv8_libbase.dylib libv8_libplatform.dylib obj snapshot_blob.bin v8_build_config.json # 新建的js示例文件 test.js
方法三:mac
# 如果已有HomeBrew,忽略第一条命令 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install v8
node --print-bytecode ./test.js
,打印出 Ignition(解释器)生成的 Bytecode(字节码)。查看 d8 命令
# 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了 ./d8 --help
过滤特定的命令
# 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量 ./d8 --help |grep print
如:
// test.js function sum(a) { var b = 6; return a + 6; } console.log(sum(3));
# d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。 ./d8 ./test.js --print-bytecode # 执行以下命令,输出9 ./d8 ./test.js
你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax
命令,你就可以在 test.js 中使用诸如HasFastProperties
(检查一个对象是否拥有快属性)的内部方法(索引属性、常规属性、快属性等下文会介绍)。
function Foo(property_num, element_num) { //添加可索引属性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常规属性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10); // 检查一个对象是否拥有快属性 console.log(%HasFastProperties(bar)); delete bar.property2; console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js # 依次打印:true false
V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:
Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。
通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系。大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
基于寄存器的解释器架构:
资料参考:解释器是如何解释执行字节码的?
其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)。
图片中的红色虚线是逆向的,也就是说Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。
function add(x, y) { return x + y; } add(3, 5); add('3', '5');
在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器。
资料拓展参考:V8 引擎是如何工作的?
V8 执行一段 JavaScript 的流程图:
资料拓展:V8 是如何执行一段 JavaScript 代码的?
V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
总结:
V8 执行一段 JavaScript 代码所经历的主要流程包括:
// 闭包(静态作用域,一等公民,调用栈的矛盾体) function foo() { var d = 20; return function inner(a, b) { const c = a + b + d; return c; }; } const f = foo();
关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。
所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,这主要是基于以下两点:
V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。
下面的代码会输出什么:
// test.js function Foo() { this[200] = 'test-200'; this[1] = 'test-1'; this[100] = 'test-100'; this['B'] = 'bar-B'; this[50] = 'test-50'; this[9] = 'test-9'; this[8] = 'test-8'; this[3] = 'test-3'; this[5] = 'test-5'; this['D'] = 'bar-D'; this['C'] = 'bar-C'; } var bar = new Foo(); for (key in bar) { console.log(`index:${key} value:${bar[key]}`); } //输出: // index:1 value:test-1 // index:3 value:test-3 // index:5 value:test-5 // index:8 value:test-8 // index:9 value:test-9 // index:50 value:test-50 // index:100 value:test-100 // index:200 value:test-200 // index:B value:bar-B // index:D value:bar-D // index:C value:bar-C
在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。
function Foo(property_num, element_num) { //添加可索引属性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常规属性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10);
可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。
我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
v8 属性存储:
总结:
因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。
通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。
但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。
资料拓展:快属性和慢属性:V8 是怎样提升对象属性访问速度的?
栈的优势和缺点:
虽然操作速度非常快,但是栈也是有缺点的,其中最大的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配一块连续的大空间是非常难的,因此栈空间是有限的。
// 栈溢出 function factorial(n) { if (n === 1) { return 1; } return n * factorial(n - 1); } console.log(factorial(50000));
继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。
JavaScript 的每个对象都包含了一个隐藏属性 __proto__
,我们就把该隐藏属性 __proto__
称之为该对象的原型 (prototype),__proto__
指向了内存中的另外一个对象,我们就把 __proto__
指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。
JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。
__proto__
var animal = { type: 'Default', color: 'Default', getInfo: function () { return `Type is: ${this.type},color is ${this.color}.`; }, }; var dog = { type: 'Dog', color: 'Black', };
利用__proto__
实现继承:
dog.__proto__ = animal; dog.getInfo();
通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__
,但是在实际项目中,我们不应该直接通过 __proto__
来访问或者修改该属性,其主要原因有两个:
__proto__
会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。
function DogFactory(type, color) { this.type = type; this.color = color; } var dog = new DogFactory('Dog', 'Black');
其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:
var dog = {}; dog.__proto__ = DogFactory.prototype; DogFactory.call(dog, 'Dog', 'Black');
随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:
这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:
// test.js function add(x, y) { var z = x + y; return z; } console.log(add(1, 2));
运行./d8 ./test.js --print-bytecode
:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)] Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this Register count 1 Frame size 8 0x10008250026 @ 0 : 25 02 Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register 0x10008250028 @ 2 : 34 03 00 Add a0, [0] 0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中 0x1000825002d @ 7 : aa Return #结束当前函数的执行,并将控制权传回给调用方 Constant pool (size = 0) Handler Table (size = 0) Source Position Table (size = 0) 3
常用字节码指令:
Add:Add a0, [0]
是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。
add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
V8 中的字节码指令集
JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。
静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。
JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。
具体地讲,V8 对每个对象做如下两点假设:
符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:
// test.js let point1 = { x: 100, y: 200 }; let point2 = { x: 200, y: 300 }; let point3 = { x: 100 }; %DebugPrint(point1); %DebugPrint(point2); %DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# =============== DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE] # V8 为 point1 对象创建的隐藏类 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) #y: 200 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE] # V8 为 point2 对象创建的隐藏类 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 200 (const data field 0) #y: 300 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE] # V8 为 point3 对象创建的隐藏类 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) } 0x1ea308284d39: [Map] - type: JS_OBJECT_TYPE - instance size: 16 - inobject properties: 1 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
在 V8 中,每个对象都有一个 map 属性,该属性值指向该对象的隐藏类。不过如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,这样有两个好处:
那么,什么情况下两个对象的形状是相同的,要满足以下两点:
// test.js let point = {}; %DebugPrint(point); point.x = 100; %DebugPrint(point); point.y = 200; %DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] ...
最佳实践
虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:
function loadX(obj) { return obj.x; } var obj = { x: 1, y: 3 }; var obj1 = { x: 3, y: 6 }; var obj2 = { x: 3, y: 6, z: 8 }; for (var i = 0; i < 90000; i++) { loadX(obj); loadX(obj1); // 产生多态 loadX(obj2); }
通常 V8 获取 obj.x 的流程:
内联缓存及其原理:
单态、多态和超态:
总结:
V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况。
回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。
通用 UI 线程宏观架构:
UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:
微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。
// 不会使浏览器卡死 function foo() { setTimeout(foo, 0); } foo();
微任务:
// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等) function foo() { return Promise.resolve().then(foo); } foo();
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
资料拓展:co 函数库的含义和用法
从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。
垃圾回收大致可以分为以下几个步骤:
第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):
V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。代际假说有两个特点:
为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。
副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域(有些地方也称作From和To空间),一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。
主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程。
由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
V8 最开始的垃圾回收器有两个特点:
由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案。
Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的JavaScript代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧:
try{} catch{}
(如果存在 try/catch
代码快,则将性能敏感的代码放到一个嵌套的函数中);演讲资料参考: Performance Tips for JavaScript in V8 | 译文 | 内网视频 | YouTube
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案
本文首发于个人博客,欢迎指正和star。