v8工作原理-JavaScript的内存机制

数据在内存中的存放、

  • js是动态语言: 在运行过程中检查数据类型,可以用同一个变量保存不同类型的数据。
    js是弱类型语言: 支持隐式类型转换

  • 内存空间:三种类型-代码空间,栈空间和堆空间。

    • 代码空间: 保持可执行代码
    • 栈空间: 即调用栈,用来存储执行上下文, 保存原始类型的数据值
    • 堆空间: 保存引用类型的值, 提高栈上下文切换的效率以及整个程序的执行效率。

JavaScript 垃圾回收机制

  • js/java垃圾回收策略: 手动回收(由代码控制),自动回收(由垃圾回收器来释放)
  • 自动回收栈数据: 通过向下移动 ESP(记录当前执行状态的指针)来销毁该函数保存在栈中的执行上下文
  • 自动回收堆数据: 通过垃圾回收器
    副垃圾回收器主要负责新生代的垃圾回收,主垃圾回收器负责老生代的垃圾回收。
  • 垃圾回收器的工作流程:
    副垃圾回收器: 标记对象区域中的垃圾,存活的对象复制到空闲区域中
    主垃圾回收器: 标记-清除算法; 标记-整理算法
    全停顿策略: 一旦执行垃圾回收算法,需要将正在执行JavaScript脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。
    优化-增量标记: 可以降低老生代的垃圾回收而造成的卡顿.
    将标记过程分为一个个子标记,同时让垃圾回收标记和JavaScript 应用逻辑交替进行,直到标记阶段完成.

v8执行机制

  • 编译器和解释器
    编译型语言: 运行前由编译器编译生成二进制文件,运行时 直接运行该二进制文件,如C/C++,Go
    解释型语言: 运行时 通过解释器对程序进行动态解释和执行,如python,js

  • V8 执行一段代码流程

  • js的性能优化
    优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上

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

v8

V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,
js虚拟机-JavaScript翻译成机器语言

js设计思想

  • 函数是一种特殊的对象,也是由一组组值和属性组成的集合,可以被赋值、作为参数,还可以作为返回值
    函数之所以成为特殊的对象,这个特殊的地方是函数可以“被调用”,所以一个函数被调用时,它不仅关联了基础的属性和值,还需要关联相关的执行上下文。

  • 函数定义的方式

    1. 函数表达式
      函数表达式在编译阶段不会发生函数提升,无法在函数表达式之前使用该函数。本质是表达式
      立即调用函数表达式是一种特别的表达式,用来封装一些变量、函数,起到变量隔离和代码隐藏的作用,e.g. a = function(){}
    2. 函数声明
      函数声明在编译阶段会发生函数提升,在内存中创建该函数对象并提升整个函数对象。本质是语句,e.g.function fname(){}
  • V8 内部是如何存储对象的-旨在提升查找效率

    1. 对象主要由三个指针构成,分别是隐藏类,Property 还有 Element。
      排序属性-对象中的数字属性,V8中称为elements;数字属性应该按照索引值大小升序排列
      常规属性-字符串属性,V8中称为properties;字符串属性根据创建时的顺序升序排列。
      常规属性的不同存储方式:对象内属性(in-object)、快属性(fast)和慢属性(slow)
      内置内属性的策略-当常规属性少于一定数量时,会直接存储到对象中
      快属性策略:线性的存储方式,比对象内属性多了一次寻址时间。
      慢属性策略:对象中的属性过多或反复添加删除属性时,降级为非线性的字典存储模式,提升了修改对象的属性的速度。
      隐藏类-每个对象都有一个隐藏类(V8 中又被称为 map),每个对象的第一个属性的指针都指向其 map 地址。map 描述了其对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少,如果添加/删除新的属性,那么需要重新构建隐藏类。
    2. 内联缓存来提升函数执行效率
      引入IC,IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,将监听到的数据写入一个称为反馈向量的结构中,包括了加载对象属性、给对象属性赋值、还有函数调用,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。
  • JavaScript 中的继承机制,
    - 原型和原型链。
    每个对象都有一个 proto 属性,该属性直接指向了该对象的原型对象,原型对象也有自己的 proto 属性,这些属性串连在一起就成了原型链。
    - 继承就是一个对象可以访问另外一个对象中的属性和方法,通过原型和原型链的方式来实现

    1. 通过关键字 new 加上构造函数实现原型继承
- V8 如何查找该变量或函数
  当在某个函数中使用某个变量时,V8 就会去作用域链中查找相关变量。作用域链的路径就是按照词法作用域来实现的,词法作用域是按照代码定义时的位置决定的,
- 类型转换:V8是怎么实现1+“2”的
  在 JavaScript 中,类型系统是依据 ECMAScript 标准来实现的。在执行加法过程中,V8 会先通过 ToPrimitive 函数,将对象转换为原生的字符串或者是数字类型。

编译流水线

  • V8 执行 JavaScript 代码时所需要的基础环境
    基础环境是由宿主提供的,包括了全局执行上下文、事件循环系统,堆空间和栈空间。
    除此之外,V8 自身会提供JavaScript 的核心功能和垃圾回收系统。

  • CPU 是怎么执行一段二进制代码的
    首先编译之后的二进制代码被加载进内存,然后CPU就按照指令的顺序,一行一行地执行。
    在执行指令的过程中,引入寄存器,将一些中间数据存放在寄存器中,加速 CPU的执行速度。
    使用寄存器的几种方式:包括加载指令、存储指令、更新指令。

  • 函数调用是如何影响到内存布局的
    调用栈来管理函数调用过程。栈在内存中是连续的数据结构,有最大容量限制,函数的嵌套调用次数过多,就会超出栈的最大使用范围,从而导致栈溢出。
    解决栈溢出的问题:使用setTimeout或Promise来改变栈的调用方式,涉及到事件循环和微任务

  • v8 的惰性解析
    所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,会跳过函数内部的代码,仅仅生成顶层代码的 AST 和字节码。加速 JavaScript 代码的启动速度.
    解析闭包:V8 解析函数时判断该函数的内部函数是否引用了当前函数内部声明的变量。如果引用了,就将该变量存放到堆中,即便当前函数执行结束之后,也不会释放该变量。

  • 执行流程
    解释执行启动快,编译执行执行快。V8 采用了一种权衡策略,在启动过程中采用解释执行,如果某段代码的执行频率超过一个值,V8就会优化编译 编译成执行效率更加高效的机器代码。


- 字节码+JIT技术
1. 生成抽象语法树(AST)和执行上下文
AST的生成: 词法分析+语法分析
2. 基于AST生成字节码
字节码就是介于 AST 和机器码之间的一种代码,需要通过解释器转换为机器码才能执行。字节码可以减少系统的内存使用。
3. 通过解释器执行字节码,通过编译器来优化编译字节码。
如果某块优化之后的代码失效了,那么编译器需要执行反优化操作。
即时编译JIT: 解释器Ignition在解释执行字节码的同时收集代码信息,当某一部分代码变热了之后,TurboFan编译器便把热点的字节码转换为机器码并保存起来,以备下次使用。

事件循环系统

事件循环系统就是 V8 的心脏,它驱动了 V8 的持续工作。事件循环系统会调度这些排队任务,保证 JavaScript 代码被 V8 有序地执行。

异步编程方案 -通过线性的方式来编写异步代码
  • V8 是如何执行回调函数的
    同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。
    大量的回调函数会打乱代码的正常逻辑,使得代码变得不线性、不易阅读,这就是我们所说的回调地狱问题。

  • Promise 充斥着大量的 then,语义化不明显,代码不能很好地表示执行流程。

  • 生成器就可以实现函数暂停和恢复,在生成器中使用同步代码的逻辑来异步代码 (实现该逻辑的核心是协程)

  • async/await。async 是一个可以暂停和恢复执行的函数,我们会在async 函数内部使用 await 来暂停 async 函数的执行。await 等待的是一个 Promise 对象,如果 Promise 的状态变成 resolve 或者 reject,那么 async 函数会恢复执行。

  • V8是如何实现微任务
    微任务其实是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。在实时性和效率之间做有效的权衡
    将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程循环地从消息队列中读取下一个宏任务。

垃圾回收机制

V8 在执行垃圾回收时,会占用主线程的资源,

  • 什么是垃圾数据: 遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。
  • 垃圾回收器:主垃圾回收器和副垃圾回收器
    主垃圾回收器负责收集老生代中的垃圾数据,副垃圾回收器负责收集新生代中的垃圾数据。
    • 主垃圾回收器回收器主要负责老生代中的垃圾数据的回收,会经历标记、清除和整理过程。
    • 副垃圾回收器采用了Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。
  • 优化垃圾回收器执行效率
    1. 并行回收,使用多个辅助线程来并行执行垃圾回收。
    2. 增量式垃圾回收,将标记工作分解为更小的块,穿插在主线程不同的任务之间执行。
    3. 并发回收,回收线程在执行JS的过程,辅助线程在后台执行垃圾回收的操作。
  • 内存问题:
  1. 内存泄漏 :主要原因是不再需要的内存数据依然被其他对象引用着-导致页面的性能越来越差-使用严格模式避免意外的全局变量,闭包中不需要引用的数据就绝不引用,
  2. 内存膨胀 :主要是由于程序员对内存管理不科学导致的-导致页面的性能会一直很差-合理规划项目,充分利用缓存等技术来减轻项目中不必要的内存占用
  3. 频繁垃圾回收:频繁使用大的临时变量-导致页面出现延迟或者经常暂停-将这些临时变量设置为全局变量。
posted @ 2021-12-20 14:52  忘川酒  阅读(548)  评论(0编辑  收藏  举报