通过jsvu
可以安装 V8:
d8 --print--ast demo.js
可以打印出代码抽象语法树
d8 --print--scope demo.js
可以打印出代码作用域
V8执行流程:
采用解析和编译两种方式,称为JIT技术
- 由解析器生成抽象语法树和相关的作用域
- 根据 AST 和作用域生成字节码,字节码是介于 AST 和机器码的中间代码
- 解析器执行字节码
函数是懒解析的,只有执行时可以进行内部解析
V8 本身也是程序,它本身也会申请内存,它申请的内存称为常驻内存,而它又将内存分为堆和栈
- 栈用于存放JS 中的基本类型和引用类型指针
- 栈空间是连续的,增加删除只需要移动指针,操作速度很快
- 栈空间是有限的,若超出栈空间内存,会抛出栈空间溢出错误
- 栈是在执行函数时创建的,函数执行完毕后,栈销毁
- 栈中压入一个全局执行上下文,长驻栈底,不会销毁
- 每个函数执行时都会创建一个执行上下文(存储函数变量、文法环境、词法环境的对象)
- 函数执行结束,立马销毁
- 当函数中存在闭包的情况下,函数的执行上下文会被销毁。函数内部被外部引用的变量会被放入堆内存中保存。
内存不连续,需要大的内存空间,使用堆
在 32 位系统下是1.4G,64 位下分配 2G
新生代内存new space
会保存一些生命周期较短的对象,但新创建的对象无法确定其生命周期,因此会先放入新生代内存中。不过它只有 32M 大小
新生代内存old space
会保存一些生命周期较长的对象。如果判断长短?两个周期的垃圾回收之后,如果数据会在新生代内存中,则将其放入老生代内存。
比较大的对象直接放入老生代。
运行时代码空间,存放 JIT已编译代码,是唯一拥有执行权限的内存
专门存储大对象,新生代存放不下,单独分配内存存储。GC 不会对其进行垃圾回收
存放对象的Map 信息,即隐藏类,它是为了提升对象属性的访问速度的。V8 会为每一个对象创建一个隐藏类,记录对象的属性布局,包括所有属性和偏移量。
何为垃圾?程序执行结束后不再需要的数据。更准确一点,从根节点GCRoot
访问不到的就是内存垃圾。
新生代中有From Space
和To Space
两块区域,新对象会放入From Space
中,如果内存满了就开始垃圾回收。
From Space
和To Space
是双端队列
的数据结构。
- 从根节点出发,
广度优先遍历
所有能到达的对象,将存活的对象(可以访问到的对象)按顺序复制到To Space
内存中 - 遍历完成后,清空
To Space
内存 From Space
和To Space
角色互换
- 经过一次GC 还存活
- 达到空间限制
优点:空间连续,不会造成内存碎片;高效
缺陷:浪费空间
浏览器不会同时采用标记清除和标记整理两种方式,在多次标记清除之后通过标记整理去处理内存碎片的问题。
标记:从根节点出发,深度优先
遍历,能够达到的表示活对象,标记为黑色,不能达到的对象表示死对象,标记为白色。
清除:清除死亡对象,释放内存
优点:不需要进行元素移动;不需要空闲区域,节省内存
缺陷:一次标记清楚后,内存空间不连续,会出现许多内存碎片
从根节点出发,深度优先
遍历,能够达到的表示活对象,标记为黑色,不能达到的对象表示死对象,标记为白色。这个过程中如果发现内存不连续,则进行元素移动,放到前一个元素后面,保证内存的连续。最后将后面的全部清除。
优点:能够解决标记清除带来的内存碎片问题,特点是一边标记一边进行内存整理。
缺陷:需要移动元素
垃圾回收机制属于宏任务
,需要 JS 主线程调用。如果执行时间过长,会造成卡顿,阻塞 JS 的执行。
为了不阻塞 JS执行,需要进行优化。优化的思想可以是将大任务拆成任务,分步执行,类似 React的fiber;也可以将一些任务放入后台,由其他线程执行,不占用主线程
开启辅助线程来执行新生代
的垃圾回收工作,并行执行回收也是会让 JS 进入全停顿的状态,主线程不能进行任何操作,只能辅助线程完成。但由于新生代
比较小,因此不会造成长时间的卡顿。
老生代内存空间又大,存储的数据又多,因此不适合并行执行的优化,仍然会占用很长的时间。因此采用分批标记,不一次性完成,在 JS 空闲的时候执行。将标记工作分为多个阶段,每个阶段都只标记一部分对象,和主线程穿插进行。
但是这样就会有一个中间状态出现了,因为除了黑色表示活对象,白色表示死对象以外,还需要有一个种来表示还未被标记的状态。
为了支持增量标记,V8 需要支持垃圾回收的暂停和恢复,因此采用黑白灰三色标记。
-
黑色表示该节点可被 GC 根节点到达,且其全部子节点已经被标记完毕。
-
灰色表示该节点可被 GC 根节点到达,但子节点还没有被标记处理,也表示目前正在处理该节点
-
白色表示该节点还未被被 GC 到达,如果 GC 结束还没有处理,就表明无法到达,没有被引用,就会被回收。
增量标记完成后,如果内存足够则不清理,等 JS 执行完毕再清理。
主线程执行过程中,辅助线程在后台完成垃圾回收工作。标记操作由辅助线程完成,清理操作由主线程和辅助线程配合完成。
并发和并行:并发是上下文快速切换;并行是同一时刻多个进程同时进行。
会产生内部泄漏的情况:
全局变量不会被垃圾回收,依次要将不再使用的全局变量及时手动设置为null
function fn(){
a = 10
}
上面的代码中,a
就是全局变量永不回收
在界面中移除 DOM 元素时,还应该相应的移除该其他的引用
<div id='container'>
<div id='box'></div>
</div>
<script>
let container = document.getElementById('container')
let box = document.getElementById('box')
document.body.removeChild(container)
</script>
上面的代码中,DOM 中已经移除了container节点,但是它还被变量container
引用,因此不会被垃圾回收。
若不手动清除定时器,则其到时执行后也不会被垃圾回收,应该用对应的函数清除定时器
function fn(){
let i = setInterval(()=>{
},1000)
clearInterval(i)
}
fn()
如果监听函数中使用到了外部对象数据,则需要进行手动清除事件监听,不然不会被垃圾回收
function fn(){
let data = [1,2,3]
class App {
handle = ()=>{
console.log(data)
}
mount(){
document.addEventListener('click', this.handle)
}
unmount(){
document.removeEventListener('click', this.handle)
}
}
let app = new App()
return app
}
let app = fn()
app.mount()
app.unmount()
不主动清除不会被主动回收
let arr = [1,2,3]
let set = new Set()
set.add(arr)
arr = null
上面的代码中,即使arr
手动指向了null
,但是数据仍然不会被垃圾回收,因为只是清除了arr
对数据对象的引用,而set
还引用了该数据,因此不会被垃圾回收,必须再手动将set
设置为null
,才能释放arr
。但是set
中其他数据的引用可能是必须保留的,因此这种情况下最好的方式是使用WeakSet
,以及WeakMap
let arr = [1,2,3]
let set = new WeakSet()
set.add(arr)
arr = null
上面的代码arr
指向的数组就会被垃圾回收,因为WeakSet
的引用是弱引用,不会阻止垃圾回收
浏览器会保存我们输出对象的信息数据引用,因此未清理的console
也会造成内存泄漏
如何知道是哪里的代码造成了内存泄漏呢?
- 刷新页面加载
- 监控 JS 堆、文档、节点、监听器、GPU
- 手动进行 GC,触发垃圾回收
查看内存占用及性能分析:Performance
<body>
<script>
let arrData = new Array(200000).fill(100)
console.log(arr)
</script>
</body>
快照对比:Memory
<body>
<button id="btn">增加内存使用</button>
<script>
let btn = document.getElementById('btn')
btn.onclick = fn()
function Person(name){this.name = name}
function fn(){
var arr = []
for (let index = 0; index < 10000; index++) {
arr.push(new Person(index))
}
console.log(arr)
}
</script>
</body>
- 全局执行上下文会一直存在于上下文执行栈中,不会被销毁
- 查找链条比较长,比较销毁性能
- 可以使用局部缓存替代全局变量
减少函数对象方法的内存占用,让对象实例都共用同一个函数对象。
- V8 会为每个对象增加一个隐藏类,如果对象发生改变(属性改变)就会重置隐藏类,而结构相同的对象会共享隐藏类。因此对象属性尽量保持一致,且尽量不要动态添加和删除属性
- 隐藏类描述了对象的结构和属性的偏移地址,可以增加查找属性的时间
结构一致,共享隐藏类
let p1 = {name:'flten',age:18}
let p2 = {name:'wall',age:20}
对象结构不稳定,隐藏类频繁重建
let p = {}
p.name = 'flten'
p.name = 22
V8 中的内联缓存会监听函数执行,记录中间数据,如果参数结构不同优化会失效。因此尽量不要使用动态参数,让参数结构保持稳定。