工作流程
- 解析器(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),我们分别称这两个区域为
From
和To
- 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值,将它们分为了两类数组索引属性
和命名属性
;遍历时优先遍历前者,在底层存储时分别存在不同的数据结构内,分别用elements
和properties
两个指针指向它们;
{a:"for",b:"bar"}; //"a"和"b"为命名属性 |
1. HiddenClasses和DescriptorArrays
每个对象都有一个关联的HiddenClass,里面存储有关对象的信息,以及从属性名称到属性索引的映射;对象的第一个字段
指向HiddenClass,HiddenClass随着对象的变化动态更新;
在属性方面,最重要的信息是第三位字段,它存储了属性的数量,以及一个指针到描述符数组。描述符数组包含有关命名属性的信息,例如名称本身和存储值的位置
- 具有相同结构(相同顺序的相同属性)的对象具有相同的 HiddenClass
- 默认情况下,添加的每个新命名属性都会导致创建一个新的 HiddenClass
- 添加整数索引属性不会创建新的 HiddenClasses
对象共有三种不同的命名属性:
- 对象内属性
- 快属性
- 慢属性
对象内属性:对象在创建的时候会初始化一定大小的空间,当对象内的属性没有超过这个大小时,这么属性会直接存储在对象本身上,可以快速访问;以这些方式存在的属性叫做对象内属性
快属性:线性数据结构的读取速度更快,因此将存储在线性结构中的属性成为快属性
慢属性:存在于自包含的属性字典中,元信息不再通过 HiddenClass 共享;慢属性允许有效的属性删除和添加,但访问速度比其他两种类型慢;倘若一个对象频繁地增删属性,V8将这些本来会存储在线性结构中的快属性降级为慢属性
2. 快慢数组
- 快数组是一种
线性的存储方式
,内部存储是连续的内存(新创建的空数组,默认的存储方式是快数组); 快数组长度是可变的
,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现- 字典模式(慢数组)创建了一个字典(HashTable)来记录映射关系,其中索引的整数值即是字典的键
2. 快慢数组转换
- 如果快数组扩容后的容量是原来的9倍以上,意味着它比HashTable形式存储占用更大的内存,快数组会转换为慢数组
- 如果快数组新增的索引与原来最大索引的差值大于1024,快数组会被转换会慢数组
- 当慢数组转换成快数组能节省不少于50%的空间时,才会将其转换
参考文章: