深入V8引擎

JavaScript代码是如何执行的

◼ JavaScript代码下载好之后,是如何一步步被执行的呢?
◼ 我们知道,浏览器内核是由两部分组成的,以webkit为例:

  • WebCore:负责HTML解析、布局、渲染等等相关的工作;
  • JavaScriptCore:解析、执行JavaScript代码;

image.png
另外一个强大的JavaScript引擎就是V8引擎。

V8引擎的执行原理

我们来看一下官方对V8引擎的定义:

  • V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。
  • 它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理 器的Linux系统上运行。
  • V8可以独立运行,也可以嵌入到任何C ++应用程序中。

image.png

V8引擎的架构

◼ V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:
◼ Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;

◼ Ignition是一个解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • 如果函数只调用一次,Ignition会解释执行ByteCode;
  • Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter

TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
  • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执 行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
  • TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit

    V8引擎的解析图

    image.png

    JavaScript代码执行过程

    ◼ 假如我们有下面一段代码,它在JavaScript中是如何被执行的呢?
    image.png

    初始化全局对象

    ◼ js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  • 该对象 所有的作用域(scope)都可以访问;

  • 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  • 其中还有一个window属性指向自己;

image.png

JS执行上下文

◼ js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
◼ 那么现在它要执行谁呢?执行的是全局的代码块:

  • 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
  • GEC会 被放入到ECS中 执行;

◼ GEC被放入到ECS中里面包含两部分内容:

  • 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会 赋值; ✓ 这个过程也称之为变量的作用域提升(hoisting)
  • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;

image.png

认识VO对象(Variable Object)

◼ 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中。
image.png
◼ 当全局代码被执行的时候,VO就是GO对象了
image.png

全局代码执行过程

全局代码执行过程(执行前)

image.png

全局代码执行过程(执行后)

image.png

函数代码执行过程

◼ 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC), 并且压入到EC Stack中。
◼ 因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?

  • 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object);
  • 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
  • 这个AO对象会作为执行上下文的VO来存放变量的初始化;

image.png

函数执行过程(执行前)

image.png

函数执行过程(执行后)

image.png

作用域和作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值;
  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;

image.png

作用域提升面试题

image.png

作业与总结

一. 整理JavaScript的代码的执行流程

  • 首先在执行前会现在堆内存中开辟一块空间(GO) 存放一些初始的值 如Number String等等
  • 还有代码中定义的一些变量 函数(在parser转成AST树的过程中存放在GO中的 )并没有赋值
  • 同时在执行代码时在执行上下文栈(ECS)中存放一个全局执行上下文(GEC) 用于执行代码
    • GO中对应的函数 也会在堆内存中开辟出空间 为 Function Object 初始一些数据(name length scope chain等)
  • 开始执行代码
  • 每个EC中有着三个重要的内容(VO scope chain 以及this)
  • VO指向对应的作用域(全局作用域(GO) 函数作用域(AO))
  • 二. 说说你对GO/AO/VO的理解以及作用域和作用域链的理解

    GO

  • Global Object JS代码在执行前会现在堆内存中创建一个全局对象(GO)

  • 用于存放一些定义好的变量方法等包含Date Array String Number setTimeout等
  • 同时有一个window属性指向自己
  • 同时在语法分析转成AST的过程中也会将一些变量 函数 存放在GO中 只是变量的初始值为undefined

AO

  • 函数在执行前会先在堆内存中创建一个AO(Activation Object)对象 里面存放这arguments 对应函数的形参 以及在函数中定义的变量 初始值为undefined

VO

  • Variable Object 在执行函数时 会在执行上下文栈(ECS)中进入一个函数执行上下文(FEC)其中有三个核心 核心之一是VO 指向的是该函数在内存中解析时创建的AO 而在全局执行上下文中指向的是GO

作用域,作用域链

  • 当进入到一个执行上下文时 执行上下文会关联一个作用域链
  • 通常作用域链在解析时就被确定 因此 作用域链域函数的定义位置有关 而与它的调用位置无关

    三. 说说V8引擎的内存管理以及垃圾回收器

    内存管理

  • JavaScript的内存管理是自动的

  • 关于原始数据类型 直接在栈内存中分配
  • 关于复杂数据类型 在堆内存中分配

垃圾回收(GC)

  • 因为内存大小是有限的 所以在内存不需要的时候 需要进行释放 用于腾出空间
  • GC对于内存管理有着对应的算法
  • 常见的算法
    • 引用计数(Reference Count)
      • 当一个对象有引用指向它时 对应的引用计数+1
      • 当没有对象指向它时 则为0 此时进行回收
      • 但是有一个严重的问题 - 会产生循环引用
    • 标记清除(Mark-Sweep)
      • 核心思路: 可达性
      • 有一个根对象 从该对象出发 开始引用到所用到的对象 对于根对象没有引用到的对象 认为是不可用的对象
      • 对于不可用的对象 则进行回收
      • 该算法有效的解决了循环引用的问题
      • 目前V8引擎采用的就是该算法
  • V8引擎为了优化 在采用标记清除的过程中也引用了其他的算法
    • 标记整理
      • 和标记清除相似 不同的是回收时 会将保留下来的存储对象整合到连续的内存空间 避免内存碎片化
    • 分代收集(Generational Collection)
      • 将内存中的对象分为两组 新的空间 旧的空间
      • 对于长期存活的对象 会将该对象从新空间移到旧空间中 同时GC检查次数减少
      • 将新空间分为from和to 对象的GC查找之后从from移动到to空间中 然后to变为from from变为to 循环几次 对于依然存在的对象 移动到旧空间中
    • 增量收集(Increment Collection)
      • 如果存在许多对象 则GC试图一次性遍历所有的对象 可能会对性能造成一定的影响
      • 所以引擎试图将垃圾收集工作分成几部分 然后这几部分逐一处理 这样会造成微小的延迟 而不是很大的延迟
    • 闲时收集(IdIe-time Collection)
      • GC只会在CPU空闲的时候运行 减少可能对代码执行造成的影响