V8工作原理

478 阅读6分钟

V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js中。我们可以简单将 JavaScript 引擎理解成是一个翻译程序,将人类能够理解的JavaScript,翻译成机器能够理解的机器语言。

栈空间和堆空间:数据是如何存储的?

首先我们先看两段代码:

function apple() {
    var a = 1
    var b = a
    a = 2
    console.log('a: ', a)
    console.log('b: ', b)
}
apple()
function foo() {
    var a = { name: 'sunny' }
    var b = a
    a.name = '晴天'
    console.log('a:', a)
    console.log('b:', b)
}
foo()

我们看一下输出结果,来分析一下。

第一段代码 a: 2,b: 1。这没什么问题。执行第二段代码,就会发现,我们仅仅只是改变了 a 的值,但是 a 和 b 的输出是一样的,b 也被修改了,这和我们预期的不一样。

JavaScript 是什么类型的语言?

int main() {
    int a = 1;
    char* b = "sunny";
    bool c = true;
    return 0;
}

对于 C 语言,我们在定义变量之前,是需要先定义类型的,而对于 JavaScript,我们就不需要再声明变量之前确认其数据类型。

在声明变量之前需要先定义变量类型。我们把这种在使用之前就需要确认其变量数据类型的成为静态语言,相反地,我们把在运行过程中需要检查数据类型的语言称为动态语言

在前面代码中,我们可以执行

c = a

将 int 类型的变量赋值给布尔值,这段代码也是可以编译执行的。因为在编译中,会发生隐式类型转换,将int型的变量悄悄转换成 bool 型的变量。支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。参考:

image.png

那么,我们现在知道了,JavaScript是一种弱类型、动态的语言

弱类型,意味着你不需要告诉JavaScript引擎变量的数据类型,它会在运行代码的时候算出来。

动态,意味这可以使用同一变量保存不同类型的数据。

JavaScript 的数据类型

JavaScript 的数据类型,8种:

类型描述
Boolean有两个值:true、false
null一个表明 null 值的特殊关键字
undefined和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性
Number整数或浮点数,例如: 42 或者 3.14159
BigInt可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制
String字符串是一串表示文本值的字符序列,例如:"sunny"
Symbol一种实例是唯一且不可改变的数据类型(ES6新增的)
Object对象,可以看作一组属性的集合

需要注意的三点内容:

  1. 使用typeof检测null类型时,返回的是 Object
  2. Object 类型比较特殊,它是由上述7种类型组成的一个包含了 key-value 对的数据类型
  3. 前7种为原始类型,最后一种为引用类型。区分这两种,是因为它们在内存中存放的位置不一样

内存空间

image.png

主要有三种类型空间:代码空间、栈空间、堆空间

原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的

因为 JavaScript 引擎需要用栈来维护程序执行期间的上下文状态,如果栈空间大了的话,所有的数据都存放在栈空间里面会影响上下文切换的效率,进而影响到整个程序的执行效率。

image.png

通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间比较大,这类数据会存放在堆中,堆的空间很大,可以存放很多大的数据,不过缺点是分配内存和回收内存会占用一定的时间。

在 JavaScript 中,赋值操作和其他语言有很大不同,原始类型的赋值会完整复制变量值,而引用类型的赋值则是复制引用地址

再谈闭包

var a = 0
var func1 = function () {
    var b = 0
    var inner = function () {
        a = a + 1
        b = b + 1
        return b
    }
    return inner
}
console.log(`outer: a=${a}`) //0
var func2 = func1();
for (var i = 0; i < 2; i++) {
    console.log(`func2: b= ${func2()}, a=${a} `) // 1,1  2,2
}

var func3 = func1();
for (var i = 0; i < 2; i++) {
    console.log(`func3: b= ${func3()}, a=${a} `) // 1,3  2,4
}

垃圾回收:垃圾是如何自动回收的?

JavaScript 是使用自动垃圾回收的策略的,它产生的垃圾数据是由垃圾回收器来释放的,不需要手动回收。

调用栈中的数据是如何回收的

image.png

当一个函数执行结束后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。

堆中的数据是如何回收的

要回收堆中的数据,就需要用到 JavaScript 中的垃圾回收器了。

代际假说和分代收集

代际假说的特点:

  1. 大部分对象在内存中存在的时间很短,简单来说,很多对象一经分配内存,很快就变得不可访问;
  2. 不死的对象会获得更久

在 V8 中会将堆分成新生代和老生代两个区域,新生代存放的是生存时间短的对象,老生代中存放生村时间就的对象。

对于这两个区域,V8 分别使用两个不同的垃圾回收器,以便高效地实施垃圾回收: 副垃圾回收器,主要负责新生代的垃圾回收 主垃圾回收器,主要负责老生代的垃圾回收

垃圾回收器的工作流程

不论什么垃圾的回收器,它们都有一套共同的执行流程

  1. 标记空间中活动对象和非活动对象。所谓活动对象,就是还在使用的对象;非活动对象,就是可以回收的对象
  2. 回收非活动对象所占据的内存。其实就是在所有的标记完成后,统一清理内存中所有被标记为可回收的对象
  3. 内存整理。当内存中中出现大量内存碎片(即不连续的内存空间),需要整理这些内存碎片

编译器和解释器:V8是如何执行一段JavaScript代码的?

解释执行,需要将先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果:

image.png

编译执行,需要先将源代码转换成中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候,直接执行二进制文件就好了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。

image.png

V8使用 JIT(Just In Time)技术,即混合使用编译器和解释器的技术。这是一种权衡策略,解释执行的启动速度块,但是执行的速度慢,而编译执行的启动速度慢,但执行时的速度快。

image.png

主要流程包括:

  1. 初始化基础环境
  2. 解析源码生成AST和作用域
  3. 依据AST和作用域生成字节码
  4. 解释执行字节码
  5. 监听热点代码
  6. 优化热点代码为二进制的机器代码
  7. 反优化生成的二进制机器代码