V8引擎学习 您所在的位置:网站首页 菲乐的产地是哪里的特产 V8引擎学习

V8引擎学习

2023-04-14 01:11| 来源: 网络整理| 查看: 265

计算机模型

在这里插入图片描述

寄存器 中央处理器的组成部分 寄存器是有限存储容量额高速存储部件 可以用来暂存指令,数据和地址 存储器内的数据可以用来执行算术和逻辑运算。 寄存器内的地址可用于指向内存的某个位置 内存 随机存取存储器也叫内存,英文缩写RAM RAM是与CPU直接交换数据的内部存储器 RAM工作时可以从任何一个指定地址写入或者读出信息 RAM在计算机中用来暂时存储程序,数据和中间结果。

在这里插入图片描述

32位操作系统支持的内存最多为2的32次方,也就是4g。

0x00000001 => … => 0xFFFFFFF

内存空间分类 数据空间 指令空间

在这里插入图片描述

指令

可以通过指令指挥计算机进行工作

机器语言指令

计算机只认识0和1,所以我们可以通过二进制指令和计算机进行沟通。

这些指令被称为指令集,也就是机器语言

MIPS是一种采取精简指令集(RISC)的处理器架构

最常见的MIPS-32位指令集每个指令是一个32位的二进制数。

如下就是一个指令 在这里插入图片描述

汇编指令

如上,二进制指令难以编写和阅读,所以出现了汇编指令集。如下

在这里插入图片描述

add $s3, $s1, $s2 //累加,带符号数累加 1 V8

V8引擎是一个js引擎实现

1.1 语言的分类 1.1.1解释执行,边解释边执行 (理解成同声翻译)

将源代码通过解析器转成中间代码,再用解释器执行中间代码,输出结果。

启动快,执行慢。

源代码 => 解析器 => 中间代码 => 解释器(执行) => 结果

在这里插入图片描述

1.1.2编译执行(理解成,翻译整本书。) 先将代码通过解析器转成中间代码,再用编译器把中间代码转成机器码,最后执行机器码,输出结果 启动慢,执行快。

在这里插入图片描述

1.2 V8执行过程 (了解)

V8采用的是解释和编译两种方式,这种混合采用的方式称为JIT技术

第一步先由解析器生成抽象语法树和相关的作用域

第二部根据AST和作用域生成字节码,字节码是介于AST和机器码的中间代码。

然后由解释器直接执行字节码,也可以让编译器把字节码编译成机器码再执行

jsvu可以快速安装v8引擎。

V8源码编译出来的可执行程序名为d8, d8是V8自己的开发工具shell

js => 解析器 => AST语法树 => 字节码 => 解释器执行 => 结果 => 编译器编译器 => 机器码 => 结果

在这里插入图片描述

js源代码会被解析器解析成ast抽象语法树和生成作用域

在这里插入图片描述

格式大概如上,如

var a = 1; var b = 2; var c = a + b;

其AST抽象语法树大概就是下面这样。

在这里插入图片描述

还可以查看作用域

在这里插入图片描述

顶层global作用域,以及三个变量,c,a,b。如果遇到函数,函数内部的内容并不会编译解析,而是等到执行的时候才去编译解析,加快首次编译速度。

1.3字节码

字节码是机器码的抽象表示 (虚拟dom跟不同平台的实现(如真实DOM,node环境等等。))

源代码直接编译成机器码时间太长,体积太大,不适合移动端。

编译成字节码编译时间短,体小

var a = 1; var b = 2; var c = a + b;

字节码的形状

在这里插入图片描述

在这里插入图片描述

2 V8内存管理 (了解) 程序运行需要分配内存 V8也会申请内存,这种内存叫做常驻内存集合 常驻内存集合又分为堆和栈 2.1 栈 (了解)

栈用于存放js中的基本类型和引用类型指针。(AO, EC(G))

栈的空间是连续的,增加删除只需要移动指针,操作速度非常快。

栈的空间是有限的,当栈满了就会抛出错误。

栈一般是执行函数时创建的,当函数执行的时候入栈,执行完毕出栈。

比包如:

function Persom(){ } function one(){ const a = new Persom() function two(){ return a } return two } debugger const a = one()() console.log(a);

因为two引用了one中的a,所以形成闭包,当执行到const a = one()()结束的时候,其实栈里面的one和two已经出栈释放了,之所以a可以取值,是因为在堆地址中,存放了一个one的闭包。如 在这里插入图片描述

可以看到,one执行完已经出栈释放了。生成闭包的条件就是two引用了one里面的变量,如果return {},是不会创建闭包的。

栈很简单,执行完毕就释放,不需要垃圾回收。栈只有爆栈的问题,当死循环的时候,栈就会溢出,因为栈的空间很小。 2.4 堆(堆才需要垃圾回收)(重点)

如果不需要连续空间,或者申请的内存较大(64位2g),可以使用堆。

堆主要是用于存储JS中的引用类型

在这里插入图片描述

2.4.1 堆空间分类 新生代(new_space) !! 新生代内存用于存放一些声明周期比较短的对象数据。 老生代 !! 老生代内存存放一些生命周期较长的对象数据 当新生代的对象进行两个周期的垃圾回收之后,如果数据还存在new_space中,则将他们放入old_space中 old_sapce又可以分为两个部分,分别是old pointer space和old data space old pointer space存放GC后存活的指针对象 old data sapce存放GC后存货的数据对象 old Space使用标记清除和标记整理的方式进行垃圾回收。 code_space 运行时代码空间 用于存放JIT已编译的代码 唯一有执行权限的内存 lager_object_space 大对象空间 为了避免大对象的拷贝,使用该空间专门存储大对象 GC不会回收这部分的内存 Map space map空间(优化的机制) 存放对象的Map信息,即隐藏类 隐藏类是为了提升对象属性的访问速度的 V8会为每个对象创建一个隐藏类,记录了对象的属性布局,包括所有的属性和偏移量。 什么是垃圾

在程序运行过程中,会用到一些数据,这些数据会放在堆栈中,但是在程序运行结束后,这些数据不再被使用,那这些数据就成了垃圾。

如,

a.b = { d:1}, a.b ={ d:2} // 堆内存会开两块空间,存放{d:1},{d:2},假设他们地址为0x001 0x002 当a.b = { d:2}后,{ d:1}是a从根节点出发,但访问不到的对象,所以0x001应该被回收。

再如

function Persom(){} window.a = new Persom() window.b = new Persom() debugger window.b = new Persom()

在这里插入图片描述

debugger过后,内存中一个Persom对象变了,另一个被回收了 在这里插入图片描述

新生代的垃圾回收

新生代内存有两个区域: 对象区域(from)和空闲区域(to)

新生代内存使用Scav enger算法来管理内存,垃圾回收的入口

]

赋值后的对象在To-Space中占用的内存空间是连续的,不会出现碎片问题。

在这里插入图片描述

新生代的垃圾回收采用的是广度优先遍历 global.a = { }; global.b = { e: { } }; global.c = { f: { }, g: { h: { } } }; global.d = { }; global.d = null;

global访问到的属性就是活的,否则就需要被回收。

步骤 1 广度优先遍历form的对象区域,从根对象触发,广度优先遍历能到达的对象,把存活的读写拷贝到to区域 情况rom区域 from和to区域角色互换。 // 广度优先 form: [a,b,c,d,e,f,g,h] => to: [a,b,e,c,f,g,h] // d被过滤了 新生代的对象可以晋升到老生代中。经过一次GC还存活的对象;或者是对象赋值到to空间的时候,to的空间达到了一定的限制。(to空间>75%) 老生代的垃圾回收 老生代的对象有些是从新生代晋升过来的,有些是比较大的对象直接分配到老生代里的,所以老生代的对象空间大,活的长 如果使用新生的的scavenge算法,会浪费一般空间,而且复制如此大的内存也会消耗很差时间。 V8在老生代中的垃圾回收策略采用的是Mark-Sweep(标记清除)和Mark-Compact(标记整理)组合 Mark-sweep标记清除

在这里插入图片描述

// 标记清除法 global.a = { }; global.b = { e: { } }; global.c = { f: { }, g: { h: { } } }; global.d = { }; global.k = { }; global.d = null; 全部: [a,b,c,d, k,e,f,g,h ] // 标记阶段:深度优先 活着: [a,b,e,c,f,g,h,'',k] //造成内存空间不连续,出现内存碎片 死的:【d】 // 清除阶段: 清除d。 Mark-Compact 标记整理

在这里插入图片描述

能有效解决标记清除带来的内存问题,将活着的对象往一端移动,效率虽然差点,但是不会生成内存碎片。

一边标记一边整理,最后将右边的去掉。

// 标记整理法 global.a = { }; global.b = { e: { } }; global.c = { f: { }, g: { h: { } } }; global.d = { }; global.k = { }; global.d = null; 全部: [a,b,c,d,k,e,f,g,h ] //整理阶段,深度优先 活着: [a,b,e,c,f,g,h,k,''] //内存空间连续,不会出现内存碎片。 死的:【d】 // 清除阶段: 清除d。

10标记清除一般会伴随一次标记整理。

优化

执行垃圾回收的时候,会执行js脚本的执行。stop the world(全停顿)

回收时间过长,就会造成卡顿

性能优化

将大任务拆分为小任务,分布执行,类似fiber 将一些任务放在后台执行,不占用主线程 js执行 => 垃圾标记,垃圾清理,垃圾整理 => js执行 新生代 -Parallel 并行执行(新生代大概只有8或者16m大小)

新生代的垃圾回收采取并行策略提升垃圾回收速度,他会开启多个辅助线程来执行新生代的垃圾回收工作。

并行执行需要的时间,等于所有辅助线程时间总和加上管理的时间

并行执行的时候也是全停顿的状态,主线程不能进行任何操作,只能等待辅助线程的完成。

这个主要应用于新生代的垃圾回收。

----辅助线程工作 ----辅助线程工作------ -----> 老生代-增量标记(空闲的时间执行,类似于requestIdleCallback)

老生代因为对象又大又多,所以垃圾回收的时间更长,采用增量标记的方式进行优化。

标记工作分为多个阶段,每个阶段只标记一部分对象,和主线程的执行穿插进行

为了支持增量标记,V8必须支持垃圾回收的暂停和恢复,采用了黑白灰三色标记法

在这里插入图片描述

引入了灰色节点之后,就可以通过有没有灰色节点来判断是否标记完成,如果有灰色节点,下一次恢复应该从灰色节点开始执行。 ----开始标记 ---- 增量标记 ---- 增量标记 ---增量标记 --整理 ------; write-barrier(写屏障) 当黑色指向白色节点的时候,就会触发写屏障,这个写屏障会把白色节点设置为灰色。 global.a = { c: 1}global.a = { c:2}如上,顺序应该是a标记为灰色,{ c:1},标记黑色,a标记黑色。突然,global.a指向一个新的对象。触发写屏障,a变成灰色。 lazy sweeping(惰性清理) 当增量标记完毕之后,如果内存够用,先不清理,等js执行完毕再慢慢清理 concuurent(并发回收 ) ![在这里插入图片描述](https://img-blog.csdnimg.cn/76e2b00508674270b3cec69772f67a39.png)

标记有辅助线程完成,主进程继续执行js,清理操作由主进程+辅助线程配合完成。

在这里插入图片描述

v8主要就是同时开四个辅助线程进行标记,然后最后再统一清理。 concurrent¶llel ; 并发和并行

在这里插入图片描述

总结:

V8的内存主要分为栈内存和堆内存,栈内存很小,基本没有垃圾回收的概念,函数执行时候入栈,执行完毕出栈,当栈中上下文太多的时候会出现爆栈。

垃圾回收主要针对于堆内存,堆内存分为新生代,老生代,大对象空间…

主要看新生代和老生代的回收机制:

新生代的垃圾回收主要采用广度优先遍历,采用两个区域,对象区域(form)和空虚区域(to),主要优化就是并行执行方法,它会开启多个辅助线程进行工作,但是主进程在这时候不能工作,需要等待垃圾回收机制结束。而新生代的对象在满足一定条件下,可以晋升为老生代的对象。 老生代对象的垃圾回收主要是深度优先遍历,有标记清除和标记整理。标记清除会带来不连续的内存,所以一般每十次标记清除就会有一次标记整理。优化即是:增量标记,因为老生代的对象太大太多,如果全部执行完毕会阻塞主进程执行js。所以采用增量标记的方法,当浏览器有空闲的时候再去进行标记,然后最后再统一清除。一般还有lazy sweeping,惰性清理。和并行回收(concurrent),并行回收主要是不阻塞主进程的执行,在主进程执行的时候开启多个辅助线程进行标记,等标记完成后才会配合主进程进行清除。V8也是这么做的。 内存泄漏 不再用到的对象内存没有被及时回收的时候,称他为内存泄漏 不合理的闭包

在这里插入图片描述

const a = ()=>{const arr = [1,2,3]return ()=>{return arr[1]}}const c = a()c() 隐士全局变量

全局变量通常不会被回收,所以要避免额外的全局变量

使用完毕后置为Null

function a(){ v = { }}a()console.log(v)v = null //置为null,会被垃圾回收。 分离的DOM

当在界面移除DOM的时候,还要移除对应的节点引用、

const a = document.getElementById('root')document.body.removeChild(a)a = null //需要置为null,才会回收root节点。 定时器

记得clear掉。

事件监听器!!!

记得移除,比如react的useEffect监听了,那么在return的时候就要移除掉。

Map,Set

Map和Set存储对象的时候不主动清除也会造成内存不自动回收。

可以采用WeakMap, WeakSet对象用来保存键值对。对于键是弱引用。如

function A(){ }const a = new A()const set = new Set([a])const map = new Map([[a,'123']])a = null

即使a置为Null,但是A依旧不会被垃圾回收,因为有set和map在引用。必须set =null; map=null;才会回收A

function A(){ }const a = new A()const set = new WeakSet([a])const map = new WeakMap([[a,'123']])a = null

a置为null后,A也会被回收了,因为是弱引用,不会阻止垃圾回收。

console

浏览器保存了我们输出对象的信息数据引用,未清理的console如果输出了对象,也会造成内存泄漏、

内存泄漏排查

1 合理利用performance面板和内存面板

click const button = document.getElementById('click') function A() { } let a = [] button.onclick = () => { let i = 0 while (i const person = new A() a.push(person) }) i++ } console.log(a); }

在这里插入图片描述

点击了按钮之后,可以很明显感觉到卡顿,而堆内存也在飙升。

合理利用快照 在这里插入图片描述

点击前的快照1.1mb

点击后的快照 在这里插入图片描述

性能优化 jsbench.me可以检测js执行好坏。 少用全局变量 全局上下文会一直存在于上下文执行栈中,无法销毁,容易内存泄漏 查找变量的链条较长,容易消耗性能 容易一引起命名冲突 确定需要使用的全局可以局部缓存。 通过原型增加方法 尽量创建对象一次搞定 V8会为每个对象分配一个隐藏类,如果对象结构发生改变就会重建隐藏欸,结构相同的对象会共用隐藏类 隐藏类描述了对象的结构和属性偏移地址,可以加速查找属性的时间 优化指南:创建对象尽量保持属性顺序一致;尽量不要动态删除和添加属性 尽量保持参数结构稳定 V8的内联缓存会监听函数执行,记录中间数据,参数结构不同会让优化失效。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有