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&parallel ; 并发和并行

总结:
  • 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引擎学习的主要内容,如果未能解决你的问题,请参考以下文章

V8 堆栈空间和垃圾回收机制

JS内存垃圾回收

深入理解V8的垃圾回收原理

V8引擎学习

说 nodeJS 建立在 V8 引擎上是啥意思?

nodejs与v8引擎