阅读 249

V8引擎原理(v8引擎是什么意思)

认识V8

什么是V8?先看看官方文档的一句话

V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others.

翻译过来就是:V8 是 Google 的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。它在 Chrome 和 Node.js 等中使用。JS中V8引擎名字取自同名V8跑车系列发动机引擎,代表大马力,速度。"8个气缸分成两组,每组4个,成V型排列"。

  • V8引擎是一个JavaScript引擎实现,最初由一些语言方面专家设计,后被谷歌收购,随后谷歌对其进行了开源。

  • V8使用C++开发,在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(V8后来又引入字节码),并且使用了如内联缓存(inline caching)等方法来提高性能。

  • V8支持众多操作系统,如windows、linux、android等,也支持其他硬件架构, 具有很好的可移植性和跨平台特性。

Node = V8 + 内置模块(大部分js编写)

V8的工作过程

这是2017年4月后官方新的图

image-20211221145153295

我们简单优化后

image-20211221145614835

当 V8 编译 JavaScript 代码时,解析器(parser)将生成一个抽象语法树。语法树是 JavaScript 代码的句法结构的树形表示形式。解释器 Ignition 根据语法树生成字节码。TurboFan 是 V8 的优化编译器,TurboFan 将字节码生成优化的机器代码。

  • 如果函数没有被调用,则V8不会去编译它。

  • 如果函数只被调用1次,则Ignition将其编译Bytecode就直接解释执行了。TurboFan不会进行优化编译,因为它需要Ignition收集函数执行时的类型信息。这就要求函数至少需要执行1次,TurboFan才有可能进行优化编译。

  • 如果函数被调用多次,则它有可能会被识别为热点函数,且Ignition收集的类型信息证明可以进行优化编译的话,这时TurboFan则会将Bytecode编译为Optimized Machine Code,以提高代码的执行性能。

图片中的红线是逆向的,Optimized Machine Code会被还原为Bytecode,这个过程叫做Deoptimization(反向优化)。这是因为Ignition收集的信息可能是错误的,比如add函数的参数之前是整数,后来又变成了字符串。生成的Optimized Machine Code已经假定add函数的参数是整数,那当然是错误的,于是需要进行Deoptimization

注意:Deoptimization非常耗性能,减少Deoptimization操作,可以使用Typescript等规范变量类型

javascript转为AST抽象语法树的例子我们可以在astexplorer.net/这个网站中进行查看。比如我们写一行代码:

const name = 'yogln' 复制代码

会被转化成这样的AST树

{   "type": "Program",   "start": 0,   "end": 20,   "body": [     {       "type": "VariableDeclaration",       "start": 0,       "end": 20,       "declarations": [         {           "type": "VariableDeclarator",           "start": 6,           "end": 20,           "id": {             "type": "Identifier",             "start": 6,             "end": 10,             "name": "name"           },           "init": {             "type": "Literal",             "start": 13,             "end": 20,             "value": "yogln",             "raw": "'yogln'"           }         }       ],       "kind": "const"     }   ],   "sourceType": "module" } 复制代码

为什么V8在2017又重新引入了字节码

我们先了解解析javascript代码执行的细节:

  1. Blink(浏览器内核)将源码交给V8引擎,Stream获取到源码并且进行编码转换

  2. Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens

  3. 接下来tokens会被转换成AST树,经过Parser和PreParser 3.1 Parser就是直接将tokens转成AST树架构 3.2 PreParser称之为预解析,为什么需要预解析呢?

  • 这是因为并不是所有的JavaScript代码,在一开始时就会被执行.那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率

  • 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行

  • 比如我们在函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析

  1. 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行。

预解析解释:

机器码占空间很大,机器码占内存过大的情况下,v8 没有办法把所有 js 代码编译成机器码缓存下来。而且即使能全部缓存,这样缓存占用的内存、磁盘空间很大,退出 Chrome 再打开时序列化、反序列化缓存所花费的时间也很长,时间、空间成本都接受不了

image-20211221160430291

所以 v8 退而求其次,只编译最外层的 js 代码,也就是上图这个例子里面绿色的部分。那么内部的代码(如上图图中的黄色、红色的部分)先不编译,那什么时候编译的呢?v8 推迟到第一次被调用的时候再编译。时间上的推移导致另外一个短板,就是代码必须被解析多次——绿色的代码一次、黄色的代码再解析一次(当 new Person 被调用)、红色的代码再解析一次(当 doWork() 被调用)。因此,如果你的 js 代码的闭包套了 n 层,那么最终他们至少会被 v8 解析 n 次。

而引入字节码之后,占空间的问题就可以得到缓解。通过恰当地设计字节码的编码方式,字节码可以做到比机器码紧凑很多。V8 引入 Ignition 字节码后,代码的内存明显降低了。

“字节码是机器代码的抽象” --- 字节码的解释执行比JS源码编译为机器代码执行要快,而且字节码占用内存比机器代码小,提前编译,所以缓存字节码可以达到既提速又能降低内存占用的作用。

V8引擎的三点性能优化

1. 隐藏类(hidden class)

隐藏类类似于C类语言的指针,表示存储的地址。创建一个新的隐藏类,将开辟一个新的存储地址。

function Person(name, age) {     this.name = name;     this.age = age; } const p1 = new Person('yogln', 18); const p2 = new Person('v8', 19) p1.mail = 'yogln@gmail.com'; p2.mail = 'v8@gmail.com'; 复制代码

初始化Person对象的时候, 最开始会创建一个C0的隐藏类,该类不带有任何属性。随后在调用构造器函数的时候,随着属性的增加,引擎会生成C1,C2的过渡隐藏类,隐藏类内部会记录属性的偏移量(offset)。之所以存在过渡隐藏类是为了在多个对象间能够共享隐藏类

image-20211221162909596

不同的初始化顺序的对象,所产生的隐藏类是不一样的。隐藏类存在于内存之中,从而加速对象的存取操作。同时,尽量在构造函数里就初始化所有对象成员,减少隐藏类的产生。

2. 内联缓存(incline caching)

内联缓存(Inline Caching)技术便是用来优化运行时查找对象及其属性的过程。

每当在特定对象上调用方法时,V8 引擎必须找到该对象的隐藏类,才能确定访问特定属性的偏移量。当同一方法两次成功调用到同一个隐藏类之后,V8会省略对隐藏类的查找,直接使用存储在内联缓存的偏移量跳转到该属性的内存地址。对于该方法的所有将来的调用,V8引擎假设隐藏类并未更改,并且使用之前查找到并存储的偏移量直接跳转到特定属性的内存地址。这就大大提高了执行速度。

详情参考:

Optimizing dynamic JavaScript with inline caches

形状和内联缓存

3. 垃圾回收(GC)

v8中,所有的JavaScript对象都是通过堆来分的。堆分为新生代内存空间和老生代内存空间,且分别使用不同的垃圾回收算法进行垃圾回收。此外,垃圾回收是渐进式的垃圾回收机制。

新生代垃圾回收算法 --- Scavenge算法(牺牲空间换取时间)

检查Form空间,如果是存活对象就会复制到To空间,非存活对象占用的空间就会直接释放,完成复制后,From空间和To空间角色发生对换。

该算法的优点是只复制存活的对象,对于新生代存活对象只占少部分,所以效率上由优异的表现;

缺点是只能使用堆内存中的一半,所以无法大规模地应用到所有的垃圾回收中。但新生代对象的生命周期较短,非常适合这个算法。

image-20211221164143274

老生代垃圾回收算法 --- Mark-Sweep & Mark-Compact

老生代不使用Scavenge算法的原因:

  • 老生代存活对象很多,复制效率很低;

  • 浪费一半空间

Mark-Sweep标记清除

Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。

  • 标记阶段:对老生代进行第一次扫描,标记活动对象

  • 清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象

image-20211221165211545

看似一切 perfect,但是还遗留一个问题,被清除的对象遍布于各内存地址,产生很多内存碎片。

Mark-Compact标记整理

由于Mark-Sweep完成之后,老生代的内存中产生了很多内存碎片,若不清理这些内存碎片,如果出现需要分配一个大对象的时候,这时所有的碎片空间都完全无法完成分配,就会提前触发垃圾回收,而这次回收其实不是必要的。

为了解决内存碎片问题,Mark-Compact被提出,它是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。

image-20211221165244294

增量标记 - Incremental marking

V8当前垃圾回收机制

2011年,V8应用了增量标记机制。直至2018年,Chrome64和Node.js V10启动并发标记(Concurrent),同时在并发的基础上添加并行(Parallel)技术,使得垃圾回收时间大幅度缩短。

副垃圾回收器

V8在新生代垃圾回收中,使用并行(parallel)机制,在整理排序阶段,也就是将活动对象从from-to复制到space-to的时候,启用多个辅助线程,并行的进行整理。由于多个线程竞争一个新生代的堆的内存资源,可能出现有某个活动对象被多个线程进行复制操作的问题,为了解决这个问题,V8在第一个线程对活动对象进行复制并且复制完成后,都必须去维护复制这个活动对象后的指针转发地址,以便于其他协助线程可以找到该活动对象后可以判断该活动对象是否已被复制。

image-20211221165752895

主垃圾回收器

V8在老生代垃圾回收中,如果堆中的内存大小超过某个阈值之后,会启用并发(Concurrent)标记任务。每个辅助线程都会去追踪每个标记到的对象的指针以及对这个对象的引用,而在JavaScript代码执行时候,并发标记也在后台的辅助进程中进行,当堆中的某个对象指针被JavaScript代码修改的时候,写入屏障(write barriers)技术会在辅助线程在进行并发标记的时候进行追踪。

当并发标记完成或者动态分配的内存到达极限的时候,主线程会执行最终的快速标记步骤,这个时候主线程会挂起,主线程会再一次的扫描根集以确保所有的对象都完成了标记,由于辅助线程已经标记过活动对象,主线程的本次扫描只是进行check操作,确认完成之后,某些辅助线程会进行清理内存操作,某些辅助进程会进行内存整理操作,由于都是并发的,并不会影响主线程JavaScript代码的执行。

image-20211221165812937

其实,大部分JavaScript开发人员并不需要考虑垃圾回收,但是了解一些垃圾回收的内部原理,可以帮助你了解内存的使用情况,根据内存使用观察是否存在内存泄露,而防止内存泄露,是提升应用性能的一个重要举措。


作者:yogln
链接:https://juejin.cn/post/7044083628267175966

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐