五、垃圾回收&内存泄漏
进度
2.1 什么是 GC?【done】
2.2 垃圾产生&为何回收【done】
2.3 垃圾回收策略【done】
2.4 V8 对 GC 的优化【to do】
3.1 什么是内存泄漏?【done】
3.2 常见的内存泄漏【done】
3.3 内存泄漏排查、定位与修复【to do】
3.4 常见的前端内存问题【done】
一. 目标
- 掌握 JS & V8 垃圾回收机制
- 掌握常见内存泄漏事件及排查方法
二、JS 的垃圾回收机制
2.1 什么是 GC?
- Garbage Collection
- 高级语言自带 GC Java Pytenon JavaScript
- 无 GC 的语言,比如 C、C++ ,需要程序员手动管理内存,相对比较麻烦。
2.2 垃圾产生&为何回收
2.3 垃圾回收策略
2.3.0 可达性
2.3.1 标记清除法
What is mark-sweep?
怎么标记变量?
1.策略
2.优点
3.缺点
标记整理法mark-compact
2.3.2 引用计数法
What is Reference Counting?
- 早先 de 垃圾回收算法
- 对象有没有其他对象引用
- 零引用
- 垃圾回收机制回收
1.策略
追踪记录每个变量值被使用的次数
let a = new Object() //1a
let b = a // 2a,b
a = null // 1b
b = null // GC
2.优点
- 立即回收垃圾(引用值 为 0 变 垃圾)
- 引用时计数(对比:标记清除算法间隔进行:JS 运行中,需要线程暂停,执行 GC;还需要遍历堆里的 AO&非 AO 来清除。引用计数只需引用时技术即可。)
3.缺点
-
计数器
- 占位置很大
- limit(number(引用)) 不知
-
循环引用——无法回收
2.4 V8 对 GC 的优化
分代式垃圾回收
并行回收(Parallel)
增量标记与懒性清理
增量
三色标记法(暂停与恢复)
写屏障(增量中修改引用)
懒性情里
增量标记与懒性清理的优缺点?
并发回收(Concurrent)
总结
三、JS 的内存泄漏
3.1 什么是内存泄漏?
3.2 常见的内存泄漏
3.2.1 不正当的闭包
- 没有造成内存泄漏的闭包
function fn1() {
let test = new Array(1000).fill('xianzao') // test 可被回收 没有内存泄漏
return function () {
// 典型闭包:是有权访问另一 fun(fn1) 函数作用域中变量的函数
console.log('zaoxian')
} // 返回函数 没有 fn1 内部的引用
}
let fn1Child = fn1()
fn1Child()
- 造成内存泄漏的闭包 & 解决方案:置空
function fn2() {
let test = new Array(1000).fill('xianzao') // test 不会被回收,造成内存泄漏
return function () {
// 闭包
console.log(test)
return test
} // return 的函数中:存在另一 fun(fn2) 函数作用域中 变量的引用 test
}
let fn2Child = fn2()
fn2Child()
fn2Child = null // 解决:函数调用后,外部引用关系置空
3.2.2 隐式全局变量
2 个额外隐式全局变量:
function fn() {
test1 = new Array(1000).fill('xianzao') // 1.没有声明(没有声明从而制造了隐式全局变量test1)
this.test2 = new Array(1000).fill('xianzao') // 2.函数 this(函数内部this指向window,制造了隐式全局变量test2)
}
fn()
置空/重新分配:
var test = new Array(10000)
// do something
test = null
3.2.3 游离 DOM 引用
- 解决图解:
-
概述: 考虑 性能/代码简洁,
使用变量缓存 DOM 节点引用,
但 移除节点时,
应该 释放缓存的引用,
否则 游离子树无法释放。 -
代码举例
<div id="root">
<ul id="ul">
<li></li>
<li></li>
<li id="li3"></li>
<li></li>
</ul>
</div>
<script>
let root = document.querySelector('#root')
let ul = document.querySelector('#ul')
let li3 = document.querySelector('#li3')
// 由于ul变量存在,整个ul及其子元素都不能GC
root.removeChild(ul) // 1.移除 root 里 ul
// 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
ul = null // 2.变量缓存 DOM 节点引用——删除节点
// 置空变量——避免额外全局变量产生
// 否则,不能GC——内存泄漏
// 已无变量引用,此时可以GC
li3 = null
</script>
3.2.4 定时器
使用了 setInterval setTimeout requestAnimationFrame ,
要调用 clearInterval clearTimeout cancelAnimationFrame 来清除,
从而避免 第一行 没有结束前(调用第二行前),回调函数本身 ② & 回调函数内依赖的变量 ①无法回收,而造成的内存泄漏问题。
// 获取数据
let someResource = getData()
setInterval(() => {
const node = document.getElementById('Node')
if(node) {
node.innerHTML = JSON.stringify(someResource)) // ① 回调函数里的变量,无法回收,即 someResource 无法回收
}
} // ② 回调函数本身,无法回收
, 1000)
3.2.5 事件监听器
Vue 举例,React 也一样:
如果没有第2步(2.组件销毁时,主动清除事件监听器组件内挂载的事件处理函数),
引用的变量/函数 被认为需要 而不进行回收;
内部引用变量存储大量数据 -> 页面占用内存过 -> 意外的内存泄漏。
<template>
<div></div>
</template>
<script>
export default {
created() {
// 事件监听器:1.组件内挂载 事件处理函数
window.addEventListener('resize', this.doSomething)
},
beforeDestroy() {
// 事件监听器:2.组件销毁,主动清除
window.removeEventListener('resize', this.doSomething)
},
methods: {
doSomething() {
// do something
}
}
}
</script>
3.2.6 Map、Set 对象
Map Set Object({})都是强引用,主动清除,否则内存泄漏。
Map: key 为对象,用 WeakMap,WeakMap 的键是弱引用&必须为对象(由于对象弱引用,不干扰 JS 的 GC),值:任意对象/原始值。Set: 用 Set 引用对象,用 WeakSet,存储对象弱引用的唯一值,值同样不重复&只保存对象弱引用(由于对象弱引用,不干扰 JS 的 GC)。- 强&弱引用:
- 强引用:JS GC 机制,持有对象引用(强引用),对象不会 GC;
- 弱引用:对象被弱引用——不可访问的(弱可访问的)——可能任何时刻被回收。
例 1 :简单重写引用
let obj = { id: 1 } // 强引用
obj = null // 重写引用,清除对象引用,可回收
例 2 :使用 Map Set
let obj = { id: 1 }
let user = { info: obj } // user 强引用 obj
let set = new Set([obj]) // set 强引用 obj
let map = new Map([[obj, 'xianzao']]) // map 强引用 obj
obj = null // 重写置空
console.log(user.info) // {id: 1} ——>依然可取——>重写所有引用置空
console.log(set)
console.log(map)
例 3 :使用 WeakMap WeakSet
let obj = { id: 1 }
let weakSet = new WeakSet([obj]) // 弱引用 obj
let weakMap = new WeakMap([[obj, 'xianzao']]) // 弱引用 obj
obj = null // 重写置空
// {id: 1} ——>下一次 GC
3.2.7 Console
生产环境——及时清理输出!
- 项目上线,不清理 console ——隐患,容易忽略;
- 控制台——数据输出,浏览器保存了——输出对象的信息数据引用;
- 未清理的 console ,if 输出对象——内存泄漏。
3.3 内存泄漏排查、定位与修复
例子:不正当使用闭包构成内存泄漏
原因:arr 数据有多少元素,存在多少次闭包引用——>程序点击次数越多,push 越多,内存消耗越大,页面越来越卡。
3.3.1 排查问题
3.3.2 分析定位
1.closure
2.array
3.总结
3.3.3 修复验证
3.4 常见的前端内存问题
- 内存泄漏
- 内存膨胀:
短时间 内存占用 急速上升 到达峰值;
技术手段减少对内存的占用。 - 频繁 GC:
GC 执行频繁,频繁使用 大临时变量 -> 新生代空间被装满,速度极快;
每次 新生代装满 -> 触发 GC(频繁 GC) -> 页面卡顿; 避免:减少 太多临时变量的情况(临时变量无用就回收)