• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

V8引擎工作原理

武飞扬头像
自驱
帮助1

*PS: 浏览器原理与实践系列文章内容来自极客时间李兵老师《浏览器工作原理与实践》,主要是记录自己的学习过程,基于自己的理解对内容做的一些总结,包括《宏观视角下的浏览器》《浏览器中的JS执行机制》《V8引擎工作原理》《事件循环系统》《浏览器中的页面》《网络协议》《浏览器安全》共七篇,此为第三篇

JS的内存机制

JS是弱类型、动态语言:

  • 弱类型:不需要告诉JavaScript引擎这个或那个变量是什么数据类型,JavaScript引擎在运行代码的时候自己会计算出来。
  • 动态:可以使用同一个变量保存不同类型的数据。

JS数据类型分类:7种基本数据类型(number,string,undefined,null,boolean,bigInt,symbol)和一种引用数据类型(object,包括:Object, Function, Array ...)。

基本数据类型储存在栈空间中,引用数据类型储存在堆空间中,引用数据类型的指针储存在栈空间。

因为JavaScript引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。比如一个函数执行结束了,JavaScript引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,当前执行完的函数执行上下文栈区空间全部回收。由于栈空间设计比较小,容易栈溢出,用于存放基本数据类型的小数据,堆空间很大,就可以存储很多大数据。

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。(深浅拷贝)

V8引擎垃圾回收机制

数据被使用之后,可能就不再需要了,这种数据称为垃圾数据。如果这些垃圾数据一直保存在内存中,那么内存会越用越多,所以需要对这些垃圾数据进行回收,以释放有限的内存空间。如果某块数据已经不需要了,却依旧保留在内存中,这种情况称为内存泄漏

JavaScript是一门自动垃圾回收的语言,产生的垃圾数据是由垃圾回收器来释放,并不需要手动通过代码来释放。

代际假说:

  • 大部分对象在内存中存在的时间很短,即很多对象一经分配,很快便变得不可访问。
  • 不死的对象,会活的更久

V8引擎的垃圾回收策略就是建立在代际假说的基础上的。V8会把堆分为分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。对于这两块区域,V8分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收:

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收器,主要负责老生代的垃圾回收。

不论什么类型的垃圾回收器,都有一套共同的执行流程

第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。

第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器。

副垃圾回收器原理:

副垃圾回收器主要负责新生区的垃圾回收。通常情况下,大多数小的对象都会被分配到新生区,所以这个区域虽然不大,但是垃圾回收还是比较频繁的。新生代中用Scavenge算法来处理。所谓Scavenge算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。

在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去

由于新生代中采用的Scavenge算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小

也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器原理:

主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

由于老生区的对象比较大,若要在老生区中使用Scavenge算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记-清除(Mark-Sweep) 的算法进行垃圾回收的。

标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。标记完成后直接将垃圾数据清除达到垃圾回收的目的。

标记过程和清除过程就是标记-清除算法,不过对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记-整理(Mark-Compact) ,这个标记过程仍然与标记-清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

V8是如何执行JS代码的

JavaScript属于解释型语言,即在每次运行时都需要通过解释器对程序进行动态解释和执行。与之对应的还有编译型语言,在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。

  1. 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
  2. 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

V8执行js过程:

  1. 生成抽象语法树(AST)和执行上下文:高级语言是开发者可以理解的语言,对于编译器或者解释器来说,它们可以理解的是AST。所以无论解释型语言还是编译型语言,在编译过程中,都会生成一个AST。(补充:AST是非常重要的一种数据结构,在很多项目中有着广泛的应用。其中最著名的一个项目是Babel。Babel是一个被广泛使用的代码转码器,可以将ES6代码转为ES5代码,这意味着可以直接用ES6编写程序,而不用担心现有环境是否支持ES6。Babel的工作原理就是先将ES6源码转换为AST,然后再将ES6语法的AST转换为ES5语法的AST,最后利用ES5的AST生成JavaScript源代码。除了Babel外,还有ESLint也使用AST。ESLint是一个用来检查JavaScript编写规范的插件,其检测流程也是需要将源码转换为AST,然后再利用AST来检查代码规范化的问题。)

  2. 生成AST需要两个过程:

    1. 第一阶段是分词,又称为词法分析,其作用是将一行行的源码拆解成一个个token。所谓token,指的是语法上不可能再分的、最小的单个字符或字符串。
    2. 第二阶段是解析,又称为语法分析,其作用是将上一步生成的token数据,根据语法规则转为AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
  3. 生成字节码:有了AST和执行上下文后,解释器Ignition就可以根据AST生成字节码,并解释执行字节码。(字节码是介于AST和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

  4. 执行代码:生成字节码之后,就进入执行阶段了。通常,如果有一段第一次执行的字节码,解释器Ignition会逐条解释执行。解释器Ignition除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。在Ignition执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器TurboFan就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

    字节码配合解释器和编译器,这种技术称为即时编译(JIT)

JavaScript性能优化

  1. 提升单次脚本的执行速度,避免JavaScript的长任务霸占主线程,这样可以使得页面快速响应交互;
  2. 避免大的内联脚本,因为在解析HTML的过程中,解析和编译也会占用主线程;
  3. 减少JavaScript文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。
    **
    **

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgajeck
系列文章
更多 icon
同类精品
更多 icon
继续加载