V8引擎学习
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了V8引擎学习相关的知识,希望对你有一定的参考价值。
计算机模型
寄存器
- 中央处理器的组成部分
- 寄存器是有限存储容量额高速存储部件
- 可以用来暂存指令,数据和地址
- 存储器内的数据可以用来执行算术和逻辑运算。
- 寄存器内的地址可用于指向内存的某个位置
内存
- 随机存取存储器也叫内存,英文缩写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: 1global.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面板和内存面板
<button id="click">click</button> <script> const button = document.getElementById('click') function A() let a = [] button.onclick = () => let i = 0 while (i < 100) const aa = new Array(100000).fill(0) aa.forEach(item => const person = new A() a.push(person) ) i++ console.log(a); </script>
点击了按钮之后,可以很明显感觉到卡顿,而堆内存也在飙升。
- 合理利用快照
点击前的快照1.1mb
点击后的快照
性能优化
- jsbench.me可以检测js执行好坏。
少用全局变量
- 全局上下文会一直存在于上下文执行栈中,无法销毁,容易内存泄漏
- 查找变量的链条较长,容易消耗性能
- 容易一引起命名冲突
- 确定需要使用的全局可以局部缓存。
通过原型增加方法
尽量创建对象一次搞定
- V8会为每个对象分配一个隐藏类,如果对象结构发生改变就会重建隐藏欸,结构相同的对象会共用隐藏类
- 隐藏类描述了对象的结构和属性偏移地址,可以加速查找属性的时间
- 优化指南:创建对象尽量保持属性顺序一致;尽量不要动态删除和添加属性
尽量保持参数结构稳定
- V8的内联缓存会监听函数执行,记录中间数据,参数结构不同会让优化失效。
以上是关于V8引擎学习的主要内容,如果未能解决你的问题,请参考以下文章