JS 内存泄漏
题目
如何检测 JS 内存泄漏?内存泄漏的场景有哪些?
垃圾回收
正常情况下,一个函数执行完,其中的变量都会是会 JS 垃圾回收。
function fn() {
const a = 'aaa'
console.log(a)
const obj = {
x: 100
}
console.log(obj)
}
fn()
但某些情况下,变量是销毁不了的,因为可能会被再次使用。
function fn() {
const obj = {
x: 100
}
window.obj = obj // 引用到了全局变量,obj 销毁不了
}
fn()
function genDataFns() {
const data = {} // 闭包,data 销毁不了
return {
get(key) {
return data[key]
},
set(key, val) {
data[key] = val
}
}
}
const { get, set } = genDataFns()
变量销毁不了,一定就是内存泄漏吗?—— 不一定
垃圾回收算法 - 引用计数
早起的垃圾回收算法,以“数据是否被引用”来判断要不要回收。
// 对象被 a 引用
let a = {
b: {
x: 10
}
}
let a1 = a // 又被 a1 引用
let a = 0 // 不再被 a 引用,但仍然被 a1 引用
let a1 = null // 不再被 a1 引用
// 对象最终没有任何引用,会被回收
但这个算法有一个缺陷 —— 循环引用。例如
function fn() {
const obj1 = {}
const obj2 = {}
obj1.a = obj2
obj2.a = obj1 // 循环引用,无法回收 obj1 和 obj2
}
fn()
此前有一个很著名的例子。IE6、7 使用引用计数算法进行垃圾回收,常常因为循环引用导致 DOM 对象无法进行垃圾回收。
下面的例子,即便界面上删除了 div1 ,但在 JS 内存中它仍然存在,包括它的所有属性。但现代浏览器已经解决了这个问题。
var div1
window.onload = function () {
div1 = document.getElementById('div1')
div1.aaa = div1
div1.someBigData = { ... } // 一个体积很大的数据。
}
以上这个例子就是内存泄漏。即,不希望它存在的,它却仍然存在,这是不符合预期的。关键在于“泄漏”。
垃圾回收算法 - 标记清除
基于上面的问题,现代浏览器使用“标记-清除”算法。根据“是否是否可获得”来判断是否回收。
定期从根(即全局变量)开始向下查找,能找到的即保留,找不到的即回收。循环引用不再是问题。
检测内存变化
可使用 Chrome devTools Performance 来检测内存变化
- 刷新页面,点击“GC”按钮
- 点击“Record”按钮开始记录,然后操作页面
- 操作结束,点击“GC”按钮,点击“结束”按钮,看分析结果
代码参考 memory-change.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>memory change</title>
</head>
<body>
<p>
memory change
<button id="btn1">start</button>
</p>
<script>
const arr = []
for (let i = 0; i < 10 * 10000; i++) {
arr.push(i)
}
function bind() {
// 模拟一个比较大的数据
const obj = {
str: JSON.stringify(arr) // 简单的拷贝
}
window.addEventListener('resize', () => {
console.log(obj)
})
}
let n = 0
function start() {
setTimeout(() => {
bind()
n++
// 执行 50 次
if (n < 50) {
start()
} else {
alert('done')
}
}, 200)
}
document.getElementById('btn1').addEventListener('click', () => {
start()
})
</script>
</body>
</html>
内存泄漏的场景
拿 Vue 来举例说明。
组件中有全局变量、函数的引用。组件销毁时要记得清空。
export default {
data() {
return {
nums: [10, 20, 30]
}
},
mounted() {
window.printNums = () => {
console.log(this.nums)
}
},
// beforeUnmount() {
// window.printNums = null
// },
}
组件有全局定时器。组件销毁时要记得清除。
export default {
data() {
return {
// intervalId: 0,
nums: [10, 20, 30]
}
},
// methods: {
// printNums() {
// console.log(this.nums)
// }
// },
mounted() {
setInterval(() => {
console.log(this.nums)
}, 200)
// this.intervalId = setInterval(this.printNums, 200)
},
beforeUnmount() {
// clearInterval(this.intervalId)
},
}
组件中有全局事件的引用。组件销毁时记得解绑。
export default {
data() {
return {
nums: [10, 20, 30]
}
},
// methods: {
// printNums() {
// console.log(this.nums)
// }
// },
mounted() {
window.addEventListener('resize', () => {
console.log(this.nums)
})
// window.addEventListener('reisze', this.printNums)
},
beforeUnmount() {
// window.removeEventListener('reisze', this.printNums)
},
}
组件中使用了自定义事件,销毁时要记得解绑。
export default {
data() {
return {
nums: [10, 20, 30]
}
},
// methods: {
// printNums() {
// console.log(this.nums)
// }
// },
mounted() {
event.on('event-key', () => {
console.log(this.nums)
})
// event.on('event-key', this.printNums)
},
beforeUnmount() {
// event.off('event-key', this.printNums)
},
}
闭包是内存泄漏吗
上述代码 genDataFns()
就是一个很典型的闭包,闭包的变量是无法被垃圾回收的。
但闭包不是内存泄漏,因为它是符合开发者预期的,即本身就这么设计的。而内存泄漏是非预期的。
【注意】这一说法没有定论,有些面试官可能会说“不可被垃圾回收就是内存泄漏”,不可较真。
答案
- 可使用 Chrome devTools Performance 检测内存变化
- 内存泄漏的场景
- 全局变量,函数
- 全局事件
- 全局定时器
- 自定义事件
- 闭包(无定论)
划重点
前端之前不太关注内存泄漏,因为不会像服务单一样 7*24 运行。
而随着现在富客户端系统不断普及,内存泄漏也在慢慢的被重视。
扩展
WeakMap WeakSet 弱引用,不会影响垃圾回收。
// 函数执行完,obj 会被销毁,因为外面的 WeakMap 是“弱引用”,不算在内
const wMap = new WeakMap()
function fn() {
const obj = {
name: 'zhangsan'
}
// 注意,WeakMap 专门做弱引用的,因此 WeakMap 只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。其他的无意义
wMap.set(obj, 100)
}
fn()
// 代码执行完毕之后,obj 会被销毁,wMap 中也不再存在。但我们无法第一时间看到效果。因为:
// 内存的垃圾回收机制,不是实时的,而且是 JS 代码控制不了的,因此这里不一定能直接看到效果。
// 函数执行完,obj 会被销毁,因为外面的 WeakSet 是“弱引用”,不算在内
const wSet = new WeakSet()
function fn() {
const obj = {
name: 'zhangsan'
}
wSet.add(obj) // 注意,WeakSet 就是为了做弱引用的,因此不能 add 值类型!!!无意义
}
fn()
wangEditor 多次销毁创建,测试内存泄漏。日常开发时可以参考这种方式
参考 examples/batch-destroy.html