JS的垃圾回收机制
JS垃圾回收机制
:
JS环境中分配的内存,一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收,一般是局部变量的值不用了,会被自动回收掉
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
垃圾回收算法
:
所谓垃圾回收,核心思想就是如何判断内存是否已经不再使用了,如果是,则视为垃圾,释放掉
常用的浏览器垃圾回收算法:引用计数 和 标记清除
引用计数:IE采用的是引用计数,它定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用,如果没有其他对象指向它了,说明该对象已经不再需要了
标记清除:现在的浏览器都已经不再使用引用计数算法了,大多是基于标记清除算法的改进算法
标记清除法:
- 标记清除算法将 “不再使用的对象” 定义为 “无法达到的对象”
- 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象
- 凡是能从根部到达的对象,都是还需要使用的。那些无法从根部出发触及到的对象被标记为不再使用,稍后进行回收
具体验证代码
1.引用计数
<script>
let person = { // person变量,存储着对象的引用,引用计数+1 // 1
name: 'ifCode',
age: 22
}
let p = person // p变量,也存储着对象的引用,引用计数+1 // 2
person = 1 // person中存1了,引用计数-1 // 1
p = null // p中存null了,引用计数-1 // 0,没有任何变量存储着对象空间的引用,这块空间就会被释放
// 引用计数的致命问题:循环引用=>内存泄漏(访问不到,也不释放)
function cycle(){
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "cycle reference"
}
cycle()
</script>
2.标记清除
<script>
// 按照标记清除算法来看,这个例子中的内存已经正确被垃圾回收了
function cycle(){
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "cycle reference"
}
cycle()
</script>
补充
关于内存泄漏
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
也就是说,如果内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏。
内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。
JavaScript 内存泄漏的一些场景
JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况。程序员要让浏览器内存泄漏,浏览器也是管不了的。
下面有些例子是在执行环境中,没离开当前执行环境,还没触发标记清除法。所以你需要读懂上面 JavaScript 的内存回收机制,才能更好理解下面的场景。
意外的全局变量
// 在全局作用域下定义
function count(number) {
// basicCount 相当于 window.basicCount = 2;
basicCount = 2;
return basicCount + number;
}
不过在 eslint 帮助下,这种场景现在基本没人会犯了,eslint 会直接报错,了解下就好。
被遗忘的计时器
无用的计时器忘记清理是新手最容易犯的错误之一。
就拿一个 vue 组件来做例子。
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
// 获取一些数据
},
},
mounted() {
setInterval(function() {
// 轮询获取数据
this.refresh()
}, 2000)
},
}
</script>
上面的组件销毁的时候,setInterval 还是在运行的,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,如下:
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
// 获取一些数据
},
},
mounted() {
this.refreshInterval = setInterval(function() {
// 轮询获取数据
this.refresh()
}, 2000)
},
beforeDestroy() {
clearInterval(this.refreshInterval)
},
}
</script>
被遗忘的事件监听器
无用的事件监听器忘记清理是新手最容易犯的错误之一。
还是继续使用 vue 组件做例子。
<template>
<div></div>
</template>
<script>
export default {
mounted() {
window.addEventListener('resize', () => {
// 这里做一些操作
})
},
}
</script>
上面的组件销毁的时候,resize 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:
<template>
<div></div>
</template>
<script>
export default {
mounted() {
this.resizeEventCallback = () => {
// 这里做一些操作
}
window.addEventListener('resize', this.resizeEventCallback)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeEventCallback)
},
}
</script>
被遗忘的 ES6 Set 成员
如果对 Set 不熟悉,可以看这里。
如下是有内存泄漏的(成员是引用类型的,即对象):
let map = new Set();
let value = { test: 22};
map.add(value);
value= null;
需要改成这样,才没内存泄漏:
let map = new Set();
let value = { test: 22};
map.add(value);
map.delete(value);
value = null;
有个更便捷的方式,使用 WeakSet,WeakSet 的成员是弱引用,内存回收不会考虑到这个引用是否存在。
let map = new WeakSet();
let value = { test: 22};
map.add(value);
value = null;
被遗忘的 ES6 Map 键名
如果对 Map 不熟悉,可以看这里。
如下是有内存泄漏的(键值是引用类型的,即对象):
let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;
需要改成这样,才没内存泄漏:
let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
map.delete(key);
key = null;
有个更便捷的方式,使用 WeakMap,WeakMap 的键名是弱引用,内存回收不会考虑到这个引用是否存在。
let map = new WeakMap();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;
被遗忘的订阅发布事件监听器
这个跟上面的被遗忘的事件监听器的道理是一样的。
假设订阅发布事件有三个方法 emit 、on 、off 三个方法。
还是继续使用 vue 组件做例子。
<template>
<div @click="onClick"></div>
</template>
<script>
import customEvent from 'event'
export default {
methods: {
onClick() {
customEvent.emit('test', { type: 'click' })
},
},
mounted() {
customEvent.on('test', data => {
// 一些逻辑
console.log(data)
})
},
}
</script>
上面的组件销毁的时候,自定义 test 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:
<template>
<div @click="onClick"></div>
</template>
<script>
import customEvent from 'event'
export default {
methods: {
onClick() {
customEvent.emit('test', { type: 'click' })
},
},
mounted() {
customEvent.on('test', data => {
// 一些逻辑
console.log(data)
})
},
beforeDestroy() {
customEvent.off('test')
},
}
</script>
被遗忘的闭包
闭包是经常使用的,闭包能给我们带来很多便利。
首先看下这个代码:
function closure() {
const name = 'xianshannan'
return () => {
return name
.split('')
.reverse()
.join('')
}
}
const reverseName = closure()
// 这里调用了 reverseName
reverseName();
上面有没有内存泄漏?
上面是没有内存泄漏的,因为name 变量是要用到的(非垃圾)。这也是从侧面反映了闭包的缺点,内存占用相对高,量多了会有性能影响。
但是改成这样就是有内存泄漏的:
function closure() {
const name = 'xianshannan'
return () => {
return name
.split('')
.reverse()
.join('')
}
}
const reverseName = closure()
在当前执行环境未结束的情况下,严格来说,这样是有内存泄漏的,name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。name 不是必须的,但是还是占用了内存,也不可被回收。
当然这种也是极端情况,很少人会犯这种低级错误。这个例子可以让我们更清楚的认识内存泄漏。
脱离 DOM 的引用
每个页面上的 DOM 都是占用内存的,假设有一个页面 A 元素,我们获取到了 A 元素 DOM 对象,然后赋值到了一个变量(内存指向是一样的),然后移除了页面的 A 元素,如果这个变量由于其他原因没有被回收,那么就存在内存泄漏,如下面的例子:
class Test {
constructor() {
this.elements = {
button: document.querySelector('#button'),
div: document.querySelector('#div'),
span: document.querySelector('#span'),
}
}
removeButton() {
document.body.removeChild(this.elements.button)
// this.elements.button = null
}
}
const a = new Test()
a.removeButton()
上面的例子 button 元素 虽然在页面上移除了,但是内存指向换为了 this.elements.button,内存占用还是存在的。所以上面的代码还需要这样写: this.elements.button = null,手动释放这个内存。