工作流程

  • 解析器(parser):将JS代码解析成抽象语法树AST
  • 解释器(Ignition):将抽象语法树(AST)解释成字节码(bytecode),同时可以解释执行字节码的能力
  • 编译器(TurboFan):编译出高效的机器代码


V8的惰性解析和预解析

V8的解析阶段会将源文本解析为抽象语法树,再交由解释器解释成字节码,事实上,解析前会经历一系列的操作;这个过程大根是先将源文本(UTF-8、Latin1、ASCII)转为UTF-16的字符流,扫描器(Scanner)通过组合底层字符流中的连续字符来构造这些标记。然后提供给解析器;

1.惰性解析

解析器在解析顶层代码过程中,如果遇到函数声明,不会跳过函数,而是对函数进行一次快速的预解析;仅生成顶层代码的AST和字节码;当然在这个过程中又引入了一个新的概念预解析,仅仅验证函数是否有语法上的错误,并且生成正确编译外部函数所需要的信息,当函数真正被调用的时候,才会完全解析并且按需编译;

2. 预解析

在预解析的过程中,最主要的工作就是确定函数内的变量是否被内部函数引用了;确定变量的分配方式;(对于顶层变量来说,都是在堆中分配的),也就是所谓的闭包;

注意:对于现在的V8引擎来说,具体到变量的内存分配是一个很复杂的过程,并不是所谓的基础类型存在于堆栈上,引用类型数据存在于堆上;V8在针对内存分配上做了大量的优化,对应我们实际工作者来说,不需要过多关注于内存的分配方式,这对我们来说是不需要的


V8垃圾回收机制

1. V8内存结构

  • New Space(新生区):大多数对象被分配在这里。新生区是一个很小的区域,垃圾回收(Scavenger)在这个区域非常频繁,与其他区域相独立。新生区会被等分成两个区域(Semi Space),我们分别称这两个区域为 FromTo
  • Old Space(老生区):老生区用来存储常驻与内存空间的数据。新生区的对象在经过两次 Scavenger(新生区的 GC 算法)后,如果识别到它还是活动对象,那么这些对象会被移动到老生区。老生区有自己的一个 GC 策略,称为 Mark-Sweep-Compact
    • Old pointer space:对其他对象有引用的对象。
    • Old data space:对其他对象没有引用的对象。
  • Large object space:这里存放体积超越其他区大小的对象。每个对象有自己mmap产生的内存。垃圾回收器从不移动大对象
  • Code space(代码区):代码对象,也就是包含JIT之后指令的对象,会被分配到这里
  • Cell区、属性Cell区、Map区:这些区域存放Cell、属性Cell和Map,每个区域因为都是存放相同大小的元素,因此内存结构很简单

2. Scavenger

Scavenger是V8中专门处理新生区的堆内存空间,新对象默认会在新生区的From空间申请内存,新生区的内存分配非常容易,只保有一个指向内存区的指针,不断根据对象的大小对其进行递增;当指针到达新空间末尾(From空间不足时);Scavenger开始工作,将活动对象移动至To空间,清空From空间,并和To空间互换身份.重复两次这样的过程,依然存活的对象会移动到老生区,这个过程发生的很快,对于JS来说影响很小

3. Mark-Sweep-Compact

标记清除算法,老生区的内存回收算法;采用深度遍历Roots,通过标记、清除、压缩来优化内存;具体原理相关文章很多,不过多介绍;


V8-对象

V8在处理JS的对象时,针对不同的key值,将它们分为了两类数组索引属性命名属性;遍历时优先遍历前者,在底层存储时分别存在不同的数据结构内,分别用elementsproperties两个指针指向它们;

{a:"for",b:"bar"}; //"a"和"b"为命名属性

{0:"for",1:"bar"}; //0,1数组索引属性
["for","bar"]; //0,1数组索引属性


1. HiddenClasses和DescriptorArrays

每个对象都有一个关联的HiddenClass,里面存储有关对象的信息,以及从属性名称到属性索引的映射;对象的第一个字段指向HiddenClass,HiddenClass随着对象的变化动态更新;

在属性方面,最重要的信息是第三位字段,它存储了属性的数量,以及一个指针到描述符数组。描述符数组包含有关命名属性的信息,例如名称本身和存储值的位置

  • 具有相同结构(相同顺序的相同属性)的对象具有相同的 HiddenClass
  • 默认情况下,添加的每个新命名属性都会导致创建一个新的 HiddenClass
  • 添加整数索引属性不会创建新的 HiddenClasses

对象共有三种不同的命名属性:

  1. 对象内属性
  2. 快属性
  3. 慢属性

对象内属性:对象在创建的时候会初始化一定大小的空间,当对象内的属性没有超过这个大小时,这么属性会直接存储在对象本身上,可以快速访问;以这些方式存在的属性叫做对象内属性

快属性:线性数据结构的读取速度更快,因此将存储在线性结构中的属性成为快属性

慢属性:存在于自包含的属性字典中,元信息不再通过 HiddenClass 共享;慢属性允许有效的属性删除和添加,但访问速度比其他两种类型慢;倘若一个对象频繁地增删属性,V8将这些本来会存储在线性结构中的快属性降级为慢属性

2. 快慢数组

  1. 快数组是一种线性的存储方式,内部存储是连续的内存(新创建的空数组,默认的存储方式是快数组);
  2. 快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现
  3. 字典模式(慢数组)创建了一个字典(HashTable)来记录映射关系,其中索引的整数值即是字典的键

2. 快慢数组转换

  • 如果快数组扩容后的容量是原来的9倍以上,意味着它比HashTable形式存储占用更大的内存,快数组会转换为慢数组
  • 如果快数组新增的索引与原来最大索引的差值大于1024,快数组会被转换会慢数组
  • 当慢数组转换成快数组能节省不少于50%的空间时,才会将其转换

参考文章:

  1. https://z3rog.tech/blog/2020/fast-properties.html#%E5%AF%B9%E8%B1%A1
  2. https://v8.dev/blog/trash-talk
  3. https://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/
  4. https://v8.dev/blog/preparser#fn1