摸鱼前端的自检(三)v8干了些什么?

v8干了些什么?

V8是被设计用来提高网页浏览器内部JavaScript执行的性能,那么如何提高性能呢?接下来我们一个个介绍它干了些什么。

有兴趣的同学可以去我的github,里面有我的分享的学习过程和blog.
github.com/193Eric


JIT

说到v8首先我们要明白什么是解释型语言和编译型语言。

  • 编译型语言,如c/c++,处理该语言实际上使用编译器直接将它们编译成本地代码,所以在使用的时候已经是可以执行代码,所以速度更快
  • 解释型语言,与编译型语言不同的是它需要一遍执行一边解析

Js语言到底是解释型语言还是编译型语言?

就像我们刚学js的时候总会听到说js是一门解释型语言,但是v8出现后,还能算吗?

JIT是什么?

JIT(Just-in-time)及时编译编译器,***编译***划重点,不是说是解释型语言吗?为什么还要编译… 这就是v8的一个优化速度的方式了,通过JIT及时编译的方式。

for(i=0; i < 1000; i++){
    sum += i;
}

编译型的语言直接把它编译成机器码,直接可以运行1000次。
解释型语言执行时会将sum += i转换(编译)一千次。对相同的代码进行一千次转换会造成非常大的性能损耗。

从上面的例子就可以看出,为什么要加入JIT,在javascript中,如果有代码要运行很多次,JIT 将把这段代码送到编译器中编译并且保存一个编译后的版本。下一次同样代码执行的时候,引擎会跳过翻译过程直接使用编译后的版本。而在真正的编译型语言中,编译器做的比JIT多的多了。

所以JIT是一个优化性能的工具,所以我觉得javascript是一个解释和编译混合的语言(或者可以称作解释型的编译语言)


隐藏类

javascript是动态语言(TS不算哈… ),由于是动态语言呢,类型不确定,每次都要去动态查询,这就造成大量的性能消耗,从而降低程序运行的速度。V8引擎为了优化这个,引入了隐藏类这个机制,针对对象进行分组。初始化个对象的时候会侯,会同时生成一个隐藏类或者查找之前已经生成的隐藏类。

我们看一个例子:

let Obj = (name,time)=>{
  this.name = name
  this.time = time
}

let obj1 = new Obj('eric','1993') // 1
let obj2 = new Obj('ming','1994') // 2

obj1.age = '26' // 3
obj1.height = "180" // 4

obj2.height = "170" //5
obj2.age = "25" // 6

分析下上段代码,其中的隐藏类的一部分用伪代码表示

  • 1和2开始都是初始化一个Obj,然后会v8会生成一个隐藏类C0,然后同时会加入两个属性(name,time),因为属性是相同的,所以在C0的基础生会生成一个C1和C2的过渡隐藏类。
    C1 = Obj{name},C2 = Obj{name,time}

  • 再看3和4,obj1添加了两个属性(age,height)所以会在C2的基础上生成C3和C4的隐藏类。C3 = obj{name,time,age}, C4 = obj{name,time,age,height}

  • 再看5和6,obj2添加了两个属性(height,age)因为属性的添加顺序和obj1不同,首先会去找Obj{name,time,height}的隐藏类,发现没有,所以在C2的基础上添加一个C5的隐藏类,然后寻找Obj{name,time,height,age}发现还是没有,所以在C5的基础上添加一个C6的隐藏类。
    C5 = obj{name,time,height} , C6 = obj{name,time,heightage}

不同初始化顺序的对象,所生成的隐藏类是不一样的。因此,在实际开发过程中,应该尽量保证属性初始化的顺序一致,这样生成的隐藏类可以得到共享。同时,尽量在构造函数里就初始化所有对象成员,减少隐藏类的产生。


内联缓存

这个很好理解,比如说我们写代码的时候,很多时候会用到缓存,查找的时候先查找缓存,如果匹配了,就从缓存里面取,如果不匹配,再按照原有流程去获取。

内联缓存就是,大致思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否是之前的隐藏类,如果是的话,直接使用之前的缓存结果,减少再次查找表的时间。

当然,如果一个对象有多个属性,那么缓存失误的概率就会提高,因为某个属性的类型变化之后,对象的隐藏类也会变化,就与之前的缓存不一致,需要重新使用以前的方式查找哈希表。


垃圾回收

  • 标记清除法
    该算法有两个阶段
    标记阶段:找到所有可访问的对象,做个标记
    清除阶段:遍历堆,把未被标记的对象回收

  • 引用计数法
    每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但无法解决对象相互循环引用的问题,如下图所示。

  • 复制算法
    在堆区中,对象分为新生代(年轻代)、老年代和永生代,而复制算法发生是发生在新生代的。

V8采用了一种分代回收的策略,将内存分为了新生代(new space)和老生代(old space)新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用 不同的垃圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件(后面会有介绍)时,会被移动到老生代(晋升)

  1. 新生代内存回收是复制算法,主要采用了Cheney算法。(采用复制的方式实现的垃圾回收算法),具体算法内容可以自行了解,这里只说个大概。
  2. 老生代内存回收 标记清除法(主要),标记整理
  3. IE9以下的js引擎对非原生对象(如BOM,DOM)的垃圾回收会采用引用计数算法,

猜你喜欢

转载自blog.csdn.net/qq_24073885/article/details/102919998
v8
今日推荐