内存生命周期
内存也是有生命周期的,不管什么程序语言,一般可以按顺序分为三个周期:
-
分配期
分配所需要的内存
-
使用期
使用分配到的内存(读、写)
-
释放期
不需要时将其释放和归还
什么是内存泄漏?
内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。
JavaScript 内存管理机制
JavaScript 内存管理机制和内存的生命周期是一一对应的。首先需要分配内存,然后使用内存,最后释放内存。
内存分配
JavaScript 定义变量就会自动分配内存的。我们只需了解 JavaScript 的内存是自动分配的就足够了。
内存使用
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
内存回收
前端界一般称垃圾内存回收为 GC
(Garbage Collection,即垃圾回收)。
内存泄漏一般都是发生在这一步,JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况,如果存在这些情况,需要我们手动清理内存。
JavaScript 的垃圾内存的两种回收方式
引用计数垃圾收集
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
看下下面的例子,“这个对象”的内存被回收了吗?
// “这个对象”分配给 a 变量
var a = {
a: 1,
b: 2,
}
// b 引用“这个对象”
var b = a;
// 现在,“这个对象”的原始引用 a 被 b 替换了
a = 1;
当前执行环境中,“这个对象”内存还没有被回收的,需要手动释放“这个对象”的内存(当然是还没离开执行环境的情况下),例如:
b = null;
// 或者 b = 1,反正替换“这个对象”就行了
标记清除法
当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则可以被回收
环境可以理解为我们的作用域,但是全局作用域的变量只会在页面关闭才会销毁。
// 假设这里是全局变量
// b 被标记进入环境
var b = 2;
function test() {
var a = 1;
// 函数执行时,a 被标记进入环境
return a + b;
}
// 函数执行结束,a 被标记离开环境,被回收
// 但是 b 就没有被标记离开环境
test();
+内存泄漏的场景
1.意外的全局变量
隐式声明的全局变量
2.被遗忘的计时器
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
// 获取一些数据
},
},
mounted() {
setInterval(function() {
// 轮询获取数据
this.refresh()
}, 2000)
},
}
</script>
上面的组件销毁的时候,setInterval
还是在运行的,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,通过clearInterval(this.refreshInterval)
3.被遗忘的事件监听器
无用的事件监听器忘记清理是新手最容易犯的错误之一。
<template>
<div></div>
</template>
<script>
export default {
mounted() {
window.addEventListener('resize', () => {
// 这里做一些操作 无用的监听函数会导致内存泄漏
})
},
}
</script>
4.被遗忘的闭包
function closure() {
const name = 'xianshannan'
return () => {
return name
.split('')
.reverse()
.join('')
}
}
const reverseName = closure()
// 这里调用了 reverseName
reverseName(); //如果函数没有被调用则存在内存泄漏
这里不存在内存泄漏。name 变量被closure返回的函数调用了。name被垃圾回收掉了
如果reverseName函数没有被调用就存在了内存泄漏。
5.脱离 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
,手动释放这个内存。