复习前端:JavaScript V8 引擎机制
V8 是谷歌推出的开源 JavaScript 引擎,它是用 C++
编写的,支持 Google Chrome
、Chromium
网络浏览器和 NodeJS,它负责与环境交互并生成字节码来运行程序。
V8 和其他引擎之间最显着的区别是它的即时 (JIT) 编译器。
如何在V8中执行一段JS代码
- 预分析:检查语法错误但不生成 AST
- 生成 AST : 词法/语法分析后,生成抽象语法树,AST 为每一行代码定义键值对。初始类型标识符定义 AST 属于一个程序,然后所有代码行将定义在主体内部,主体是一个对象数组。
- 生成字节码:基线编译器(Ignition)将 AST 转换为字节码
- 生成机器代码:优化编译器 (Turbofan) 将字节码转换为优化的机器代码。另外,在逐行执行字节码的过程中,如果一段代码经常被执行,V8会直接将这段代码转换并保存为机器码,下次执行不需要经过字节码,优化了执行速度
引用计数和标记清除简介
- 引用计数:如果一个变量被分配了一个引用类型,那么这个对象的引用次数是
+1
。如果变量变为另一个值,则对象的引用数为-1
,垃圾回收器将回收引用数为0
的对象。但是,当对象被循环引用时,引用数永远不会归零,导致无法释放内存。 - 标记清除:垃圾收集器首先标记内存中的所有对象,然后从根节点开始遍历,清除被引用对象和运行环境中对象的标记,剩下的标记对象不可访问,等待回收对象。
V8如何进行垃圾回收
JavaScript 引擎中变量的存储位置主要有两个,栈内存和堆内存。
- 栈内存:存放基本类型数据和引用类型数据的内存地址
- 堆内存:存放引用类型数据。
对于不同类型的变量,栈内存和堆内存垃圾回收方式不同。
- 栈内存的回收:调用栈上下文切换后回收栈内存,比较简单
- 堆内存回收:V8的堆内存分为新生代内存和老年代内存。新生代内存是临时分配的内存,存在时间短,老年代内存存在时间长。
新一代内存回收机制
新一代内存容量较小,64
位系统下只有32M
,新生代的记忆分为两部分:From
和 To
。进行垃圾回收时,先扫描 From
,回收非存活对象,存活对象按顺序复制到 To
,然后交换From/To
,等待下一次回收
老年代内存回收机制
- Promotion:如果新生代的变量经过多次回收后仍然存在,则将其放入老年代的内存中
- 标记清除:老年代内存会先遍历所有对象并标记,然后对正在使用或强引用的对象取消标记,回收标记的对象
- 内存碎片整理:将对象移动到内存的一端
为什么JS比C++等语言慢,V8做了哪些优化?
JS 的问题
- 动态类型:每次访问属性/查找方法时,都需要先检查类型;另外,动态类型在编译阶段很难优化
- 属性访问:在
C++/Java
等语言中,方法、属性都是保存在数组中,只能通过数组位移获取,而JS是保存在对象中,每次获取都要进行hash
查询。
针对 V8 的优化
优化的JIT(即时编译):与 C++/Java
等编译型语言相比,JS是同时解释和执行的,效率低下。V8 优化了这个过程:如果一段代码被执行多次,V8 会将这段代码转换成机器码并缓存起来,下次运行时直接使用机器码。
隐藏类:对于 C++
等语言,只需几条指令就可以通过偏移获取变量信息,而JS需要进行字符串匹配,效率低下。V8借用类和偏移位置的思想,将对象划分为不同的组,即隐藏类。
嵌入式缓存:即缓存对象查询的结果。一般的查询过程是:获取隐藏类地址->根据属性名找到偏移值->计算属性地址,嵌入式缓存就是这个过程结果的缓存。
总结
每当讨论 JavaScript 的工作原理时,都会谈论事件循环、微任务、宏任务和回调队列。然而,所有这些东西都没有在 JavaScript 中实现。相反,它们是 V8 引擎的一部分,负责优化 JavaScript 代码。