<浏览器>v8工作原理

v8工作原理

堆空间、栈空间

语言类型

  • 强类型:不支持隐式类型转换
  • 弱类型:支持隐式类型转换
  • 静态类型:使用前需要确认变量类型
  • 动态类型:使用过程中需要检查变量类型

js数据类型

  • 原始类型:String\Number\Boolean\Null\Undefined\Symbol\BigInt
  • 引用类型:Object

js内存空间

  • 代码空间:存储可执行代码

  • 栈空间:js用栈空间维护执行程序期间上下文的状态,原始类型保存在栈空间,通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。

  • 堆空间:引用类型保存在堆空间,引用类型一般占用空间大,堆空间很大,能存放很多大的数据,内存回收占用更多时间。

  • 原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

  • 如果所有数据都存储在栈空间,会影响上下文切换效率,从而影响程序执行效率。

闭包原理

  • 当js引擎发现闭包引用时,会在堆空间中创建闭包对象,用来保存闭包引用的值,执行上下文中创建闭包内部变量,引用闭包对象的地址。

垃圾回收

  • js中产生的垃圾是由垃圾回收器来释放的,不需要手动通过代码释放。

调用栈中的垃圾回收

  • js函数执行时,会有一个记录当前执行上下文的指针(成为ESP),指向调用栈中正在执行函数的执行上下文,表示正在执行的函数。
  • 当函数执行完毕,执行流程进入新的函数,此时要销毁老函数的执行上下文,此时js的ESP会移动到新函数的执行上下文,这个下移操作就是销毁老函数执行上下文的过程。
  • JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。

堆中的垃圾回收

  • 堆中的垃圾数据回收利用js的垃圾回收器。

  • 代际假说:

  1. 大部分内存存在时间很短,很多对象一分配内存,很快就不可以访问
  2. 不死的对象,会存活很久
  • 基于代际假说,v8来实现垃圾回收。

  • 在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

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

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

  • 垃圾回收器工作流程

  1. 标记空间中活动和非活动对象
  2. 回收非活动对象的内存,在标记完成后,统一清理可回收对象
  3. 内存整理,整理不连续空间(内存碎片),防止分配较大连续内存时内存不足。
  • 副垃圾回收器执行过程
  1. 新生代采用Scavenge 算法来处理,将新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
  2. 新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
  3. 垃圾回收过程:对对象区域中的垃圾做标记-》垃圾清理阶段,将存活的对象复制到空闲区域中,同时排列这些对象,完成整理工作-》复制完成后,对象区域和空闲区域进行对调,完成垃圾回收。
  4. 角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
  5. 为了执行效率,一般新生区的空间会被设置得比较小,js采用对象晋升策略,经过两次垃圾回收依然存在的对象,移动到老生区。
  • 主垃圾回收器执行过程
  1. 主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。
  2. 标记阶段:从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
  3. 清除阶段:清除掉标记的垃圾数据
  4. 清除算法会产生大量不连续的内存碎片,使用标记-整理算法整理内存,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 全停顿
  1. 一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
  • 增量标记
  1. 了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。
  2. 增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

编译器解释器

编译器(Compiler)

  • 编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
  • 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。

解释器(Interpreter)

  • 而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
  • 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

抽象语法树(AST)

字节码(Bytecode)

即时编译器(JIT)

v8如何执行代码

  1. 生成抽象语法树(AST)和执行上下文(代码在执行过程中的环境信息)
  • 生成 AST 需要经过两个阶段。第一阶段是分词(tokenize),又称为词法分析,将一行行的源码拆解成一个个 token,第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。
  1. 生成字节码
  • 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
  1. 执行代码
  • 如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码。
  • 即时编译(JIT):字节码配合解释器和编译器的技术。具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

性能优化

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