# v8 执行 JS

alt text

# v8 引擎介绍

Blink 内核用于解析 HTML、DOM、CSS 渲染、嵌入了 v8 引擎用于解析 Javascript

V8 是谷歌的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。它被用于 Chrome 和 Node.js 等。用于解析并执行 JavaScript 代码。

# 执行过程

  • 初始化运行环境
    • 堆栈空间
    • 全局执行上下文
    • 全局作用域
    • 事件循环系统
  • 利用 Scanner 扫描器将输入的代码词法分析成 tokens
    • 分析的结果就是一个个的 tokens 对象组成的数组
    • 分析的过程利用了有限自动状态机的概念
  • 利用 parser 解析器将 tokens 转化为抽象语法树
    • 根据分析 tokens 构造出一种树形关系结构
    • 预解析:在 JS 代码执行之前对代码进行可选的预处理,用于提高执行效率
    • 延迟解析:只有代码执行到的部分才会去解析,节省了不必要的时间和开销,提高了 JavaScript 的执行效率,其中 vite 脚手架就是利用了这个优点(还有 esbuild 打包快的优势),提高了效率。
  • 利用 ignation解释器 将 AST(抽象语法树)转为字节码(不直接转为机器码?)
    • 当年 v8 以超越同行 10 倍的运行速度而备受青睐,其本质原因是当时 v8 将 JS 源码直接编译为机器码,使得首次运行速度和后续执行速度都很快,但也存在一个问题就是内存占用太大,并且编译时占用太多时间
    • 字节码使得 v8 能够很好的进行优化与反优化,当执行代码时,对机器码的存储和复用等操作时都十分繁琐 (存储占用大,分析繁琐等), 但是分析字节码就更加容易一些
    • 字节码跨平台能力强
    • 字节码更快的加载和解析执行
    • 动态优化易操作
    • 代码安全性
  • 利用 TurboFan编译器 将字节码转为 CPU 和 ARM 识别的机器码

# v8 中的对象结构

# 常规属性和排序属性

  • v8 中的对象主要分为三个指针构成的,分别是:隐藏类、常规属性和排序属性。
  • 对象的属性数字会从小到大排列,字符串会按照原创建顺序
  • 对象中数字属性被称为排序属性,字符串属性被称为常规属性

# 快属性和慢属性

  • JS 对象很多属性是在原型链上进行查找,这样就会很慢,v8 将部分常规属性(10 个)直接存储到对象本身(对象内属性),以提高属性的查询效率。
  • 快属性容量是 10 个。

# 封装、继承、多态

  • 封装就是将抽象出来的数据和对数据的操作封装在一起,数据在内部被保护,程序其他部分只有通过成员才能对数据进行操作
  • 继承:有原型链继承、寄生继承和 call、apply 借用法继承。
  • 多态就是函数重载:同一个函数可以根据调用的情况(参数类型和数量等)来进行不同的操作。

# 隐藏类

JavaScript 是一门动态语言,其各种不确定性导致 JavaScript 的执行效率要远低于静态语言,V8 为了提升 JavaScript 的执行效率,借鉴了很多静态语言的特性,比如:JIT 机制,为了加速运算而引入了内联缓存,为了提升对象的属性访问速度而引入了隐藏类。

隐藏类是 V8 引擎在运行时自动生成和管理的数据结构,用于跟踪对象的属性和方法,相当于提前定义好对象的形状,以便于提高操作对象的效率。

# 原因

  • 当 JavaScript 运行时,例如查找对象上的某一个属性,V8 引擎会通过快慢属性去查找,整个过程非常耗时,因为 V8 在使用一个对象时,并不知道对象的具体形状(属性方法等)
  • 而 C 在声明一个对象前就需要定义该对象的结构,C 代码执行前是需要被编译的,编译时对象的结构就已经固定,也就是当代码执行时,对象的形状是无法改变
  • 所以 V8 引入了隐藏类的概念,用于跟踪对象的属性和方法以此在内存中快速查找对象属性

# 介绍

隐藏类就是把 JavaScript 的对象也进行静态化, 我们假设这个对象不会删除和新增 ,这样形状就固定了

满足条件之后 V8 就会创建隐藏类,在这个隐藏类会创建对象的基础属性

在 V8 引擎中,每个隐藏类都有一个编号( map id ),用于唯一标识该隐藏类

举个例子,假设我们有以下两个对象:

let obj1 = { name: 1, age: 2 };
let obj2 = { name: 1, age: 2, address: 3 };

这两个对象具有相同的形状,即都有属性 nameage ,但 obj2 还额外有一个属性 address 。V8 会为它们生成两个不同的隐藏类

// 隐藏类1:包含属性name和age
HiddenClass_1
├── map_id: 1
├── property_names: ['name', 'age']
├── transitions: {}
└── prototype: Object.prototype
// 隐藏类2:包含属性name、age和address
HiddenClass_2
├── map_id: 2
├── property_names: ['name', 'age', 'address']
├── transitions:
│   ├── a: HiddenClass_1
│   ├── b: HiddenClass_1
│   └── c: null
└── prototype: Object.prototype

可以看到,隐藏类 1 包含属性 nameage ,没有过渡表;而隐藏类 2 包含属性 nameageaddress ,其中属性 nameage 的过渡表指向隐藏类 1,属性 address 没有过渡表,表示该属性是新添加的

如果两个对象属性一样呢?

如果两个对象具有相同的属性,它们将共享同一个隐藏类。具体来说,当两个对象的属性顺序和类型都相同时,V8 会为它们生成一个共享的隐藏类。

举个例子,假设我们有以下两个对象:

let obj1 = { name: 1, age: 2 };
let obj2 = { name: 1, age: 2 };

这两个对象具有相同的形状,即都有属性 nameage ,且属性的顺序和类型完全一致。V8 会为它们生成一个共享的隐藏类,如下所示:

HiddenClass_1
├── map_id: 1
├── property_names: ['name', 'age']
├── transitions: {}
└── prototype: Object.prototype

可以看到,隐藏类 1 包含属性 nameage ,没有过渡表,而且两个对象都 共享 这个隐藏类。

这种共享隐藏类的机制可以节省内存空间,因为不同的对象可以共享相同的隐藏类结构。

JavaScript隐藏类机制

# v8 引擎垃圾回收

首先垃圾回收机制是对于 引用数据类型而言的 ,普通数据类型由于不知道后续是否会引用某个变量导致不能轻易将变量进行销毁

# 标记清除法

标记清除法是目前在 JS 引擎中最常用的算法,该算法分为 标记清除 两个阶段,标记阶段将所有 活动对象做上标记 ,默认标记为 0, 清除阶段将 没有标记的活动对象进行清除 ,也就是销毁掉标记为 0 的对象

  • 优点:实现简单
  • 缺点:清除之后由于剩余对象的内存位置不变,就会出现 内存碎片 ,这时候为了分配到合适的位置就要进行内存利用的算法判断, 导致分配效率慢

# 引用计数法

引用计数法的策略是跟踪每个对象被使用的次数,当对象被其他变量引用时,它的 引用次数就会加1 , 引用次数为 0 就表示没有变量在使用它,就可以将其清除

  • 优点:清晰,并且可以立即回收垃圾
  • 缺点:需要一个计数器,同时计数器需要占很大的位置,因为 引用数量的上限可能会很大 ,同时最重要的是无法解决 循环引用而无法回收的问题

# 分代式垃圾回收

V8 中将堆内存分为新生代和老生代两区域,采用不同的策略管理垃圾回收
新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大

# 新生代

新生代堆内存一分为二: 使用区空闲区 ,新加入的对象就会放在使用区,当 使用区快写满时,就开始进行垃圾回收 ,新生代垃圾回收器会对使用区中的 活动对象做标记 ,标记完成之后 将使用区的 活动对象 复制进空闲区 并进行排序,随后进入垃圾清理阶段,即 将非活动对象占用的空间清理掉 。最后 进行角色互换 ,把原来的使用区变成空闲区,把原来的空闲区变成使用区。当一个对象经过 多次复制后依然存活 ,它将会被认为是生命周期较长的对象,随后 会被移动到老生代中 ,采用老生代的垃圾回收策略进行管理
另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中
新生代采用复制方式的原因是因为:新生代中的大多数对象都是很快变为垃圾 (需要进行清除), 如果直接原地清除就要频繁清理对象, 只复制活动对象到空闲区之后就可以直接清除整个使用区 ,提高了清除效率

# 老生代

对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,如果再如新生代一般分区然后复制来复制去就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是 标记清除法
而标记清除法造成的 内存碎片问题 采用标记整理算法进行优化.

# 总结

分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查, 新老生代的回收机制及频率是不同的 ,可以说此机制的出现很大程度 提高了垃圾回收机制的效率

# 并行回收

新生代对象空间采用并行策略,在执行垃圾回收时,会启动多个线程来负责垃圾清理,以此增加效率

并行和并发:并发是:一个处理器同时处理多个任务 (并发是逻辑上同时发生), 并行是:多个处理器同时处理多个不同任务 (并行是物理上同时发生)

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝