v8引擎与垃圾回收机制
总览
v8引擎解析js代码过程,垃圾回收机制,内存泄漏笔记总结
# 浏览器内核简单介绍
浏览器内核分或两部分:渲染引擎
和JS引擎
。
渲染染引擎
:将代码转换成页面输出到浏览器界面。
JS引擎
:解析和执行javascript来实现网页的动态效果。
最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
# V8工作机制
V8 是 Google 的开源高性能 JavaScript 和 WebAssembly 引擎
浏览器拿到源代码,先创建运行环境,提取上下文,在将可执行的代码转换为ast树,也就是解释器可以识别的数据结构,解释器拿到ast树之后将其转换为字节码,并执行。正常就是这样的一个流程。从源代码到计算机的执行。
Parse模块会将JavaScript代码转换成AST(抽象语法树)
- 这是因为解释器并不直接认识JavaScript代码,如果函数没有被调用,那么是不会被转换成AST的,
Ignition解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition会执行解释执行ByteCode
TurboFan编译器,是 V8 的优化编译器,TurboFan
将字节码生成优化的机器代码。
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化,
Optimized Machine Code
会被还原为Bytecode
,这个过程叫做Deoptimization
(反向优化)。这是因为Ignition
收集的信息可能是错误的,比如add函数的参数之前是整数,后来又变成了字符串。生成的Optimized Machine Code
已经假定add函数的参数是整数,那当然是错误的,于是需要进行Deoptimization
。
# 字节码与预解析
机器码占空间很大,机器码占内存过大的情况下,v8 没有办法把所有 js 代码编译成机器码缓存下来。而且即使能全部缓存,这样缓存占用的内存、磁盘空间很大,退出 Chrome 再打开时序列化、反序列化缓存所花费的时间也很长,时间、空间成本都接受不了,所以 v8
退而求其次,只编译最外层的 js 代码,也就是上图这个例子里面绿色的部分。那么内部的代码(如上图图中的黄色、红色的部分)先不编译,那什么时候编译的呢?v8
推迟到第一次被调用的时候再编译。时间上的推移导致另外一个短板,就是代码必须被解析多次——绿色的代码一次、黄色的代码再解析一次(当 new Person 被调用)、红色的代码再解析一次(当 doWork() 被调用)。因此,如果你的 js 代码的闭包套了 n 层,那么最终他们至少会被 v8 解析 n 次。
而引入字节码之后,占空间的问题就可以得到缓解。通过恰当地设计字节码的编码方式,字节码可以做到比机器码紧凑很多。V8
引入 Ignition
字节码后,代码的内存明显降低了。
“字节码是机器代码的抽象” --- 字节码的解释执行比JS源码编译为机器代码执行要快,而且字节码占用内存比机器代码小,提前编译,所以缓存字节码可以达到既提速又能降低内存占用的作用。
# 官方解析图
Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换
Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens
接下来tokens会被转换成AST树,经过Parser和PreParser:
- Parser就是直接将tokens转成AST树架构
- PreParser则是预解析
- 因为不是所有Javascript代码在一开始必须全部解析执行,这样会影响网页运行效率
- 所以V8引擎就实现了**Lazy Parsing(延迟解析)**的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行
- 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析
生成AST树后,会被Ignition转成字节码(bytecode)匹配对应平台CPU指令进行代码执行
# 一段代码如何解析
当js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
- 其中还有一个window属性指向自己
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈
当它执行全局代码块的时候会构建一个全局执行上下文 Global Execution Context(GEC)
GEC会 被放入到ECS中 执行,其中包含两部分内容
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中, 但是并不会赋值,此过程也叫变量的作用域提升(hoisting)
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context, 简称FEC),并且压入到EC Stack中
FEC中包含三部分内容:
- 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)
- AO中包含形参、arguments、函数定义和指向函数对象、定义的变量
- 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找
- 第三部分:根据不同情况this绑定值
# 垃圾回收机制
垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的内存管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。
在js内存管理策略中它会对失去可达性(无引用或者无法通过某种方式进行访问)的空间进行回收。既失去可达性的内存空间会被视为是垃圾
let ceshi = {
a: 1,
b: 2
}
// 上方引用地址无变量引用导致白白占用内存空间
ceshi = [1, 2, 3, 4, 6]
2
3
4
5
6
7
如果没有GC,程序员必须自己手动进行内存管理,必须清楚地确保必要的内存空间,释放不要的内存空间。程序员在手动进行内存管理时,申请内存尚不存在什么问题,但在释放不要的内存空间时,就必须一个不漏地释放。这非常地麻烦。
# 标记清除
标记清除分为:标记阶段和清除阶段。
首先它会遍历堆内存上所有的对象,分别给它们打上标记,然后在代码执行过程结束之后,对所使用过的变量取消标记。在清除阶段再把具有标记的内存对象进行整体清除,从而释放内存空间。
经过标记清除策略整理后,老生代内存中因此产生了许多内存碎片,如果不进行清理内存碎片,就会对存储造成影响。
标记整理(Mark-Compact)算法 就可以有效地解决标记清除的两个缺点。它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。
# 内存泄漏
在代码中创建对象和变量时会占据内存,但是JS基于自己的内存回收机制是可以确定哪些变量不再需要,并将其进行清除。但是,当你的代码中存在逻辑缺陷时,你以为你已经不需要,但是程序中还存在这引用,这就导致程序运行完后并没有进行合适的回收所占有的内存空间。运行时间越长占用内存越多,随之出现的问题就是:性能不佳、高延迟、频繁崩溃。
过多的缓存。及时清理过多的缓存。
滥用闭包。尽量避免使用大量的闭包。
定时器或回调太多。与节点或数据相关联的计时器不再需要时,DOM节点对象可以清除,整个回调函数也不再需要。可是,计时器回调函数仍然没有被回收(计时器停止才会被回收)。当不需要setTimeout或setInterval时,定时器没有被清除,定时器的糊掉函数以及其内部依赖的变量都不能被回收,会造成内存泄漏。解决方法:在定时器完成工作时,需要手动清除定时器。
太多无效的DOM引用。DOM删除了,但是节点的引用还在,导致GC无法实现对其所占内存的回收。解决方法:给删除的DOM节点引用设置为null。
**滥用全局变量。**全局变量是根据定义无法被垃圾回收机制进行收集的,因此需要特别注意临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,请确保将其指定为null或在完成后重新分配它。解决方法:使用严格模式。
**从外到内执行appendChild。**此时即使调用removeChild也无法进行释放内存。解决方法:从内到外appendChild。
反复重写同一个数据会造成内存大量占用,但是IE浏览器关闭后会被释放。
注意程序逻辑,避免编写『死循环』之类的代码。
DOM对象和JS对象相互引用。
# 参考
JS高级之V8引擎和代码执行原理 - 掘金 (juejin.cn) (opens new window)
V8引擎的工作原理 - 掘金 (juejin.cn) (opens new window)
V8引擎原理 - 掘金 (juejin.cn) (opens new window)
图解JavaScript的垃圾回收机制 - 掘金 (juejin.cn) (opens new window)