这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
这篇文章是针对浏览器的 JavaScript 脚本,Node.js 大同小异,这里不涉及到 Node.js 的场景。当然 Node.js 作为服务端语言,必然更关注内存泄漏的问题。
用户一般不会在一个 Web 页面停留太久,即使有一点内存泄漏,重载页面内存也会跟着释放。而且浏览器也有自动回收内存的机制,所以也不会特别关注内存泄漏的问题。
但是作为开发人员的我们如果对内存泄漏没有什么概念,有时候还是有可能因为内存泄漏,导致页面卡顿。了解了内存泄漏,就知道该如何避免内存泄漏,这也是我们提升前端技能的必经之路。
目录
内存的概念
内存的生命周期
内存泄漏是如何产生的
造成内存泄漏的场景
如何查找内存泄漏
内存的概念
内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。内存(Memory)也被称为[内部存储器]其作用是用于暂时存放CPU中的运算数据,以及与[硬盘]等[外部存储器]交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。 内存是由[内存芯片]、电路板、[金手指]等部分组成的。
内存的生命周期
内存也是有生命周期的,不管什么程序语言,一般可以按顺序分为三个周期:
-
分配期
分配所需要的内存
-
使用期
使用分配到的内存(读、写)
-
释放期
不需要时将其释放和归还
内存分配 -> 内存使用 -> 内存释放。
内存泄漏是如何产生的
造成内存泄漏的场景
如何查找内存泄漏
什么是内存泄漏?
如果内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏。
内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。
JavaScript 内存管理机制
JavaScript 内存管理机制和内存的生命周期是一一对应的。首先需要分配内存,然后使用内存,最后释放内存。
其中 JavaScript 语言不需要程序员手动分配内存,绝大部分情况下也不需要手动释放内存,对 JavaScript 程序员来说通常就是使用内存(即使用变量、函数、对象等)。
内存分配
JavaScript 定义变量就会自动分配内存的。我们只需了解 JavaScript 的内存是自动分配的就足够了。
看下内存自动分配的例子:
// 给数值变量分配内存
let number = 123;
// 给字符串分配内存
const string = "xianshannan";
// 给对象及其包含的值分配内存
const object = {
a: 1,
b: null
};
// 给数组及其包含的值分配内存(就像对象一样)
const array = [1, null, "abra"];
// 给函数(可调用的对象)分配内存
function func(a){
return a;
}
内存使用
根据上面的内存自动分配例子,我们继续内存使用的例子:
// 写入内存
number = 234;
// 读取 number 和 func 的内存,写入 func 参数内存
func(number);
内存回收
前端界一般称垃圾内存回收为 GC
(Garbage Collection,即垃圾回收)。
内存泄漏一般都是发生在这一步,JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况,如果存在这些情况,需要我们手动清理内存。
以前一些老版本的浏览器的 JavaScript 回收机制没那么完善,经常出现一些 bug 的内存泄漏,不过现在的浏览器基本都没这些问题了,已过时的知识这里就不做深究了。
这里了解下现在的 JavaScript 的垃圾内存的两种回收方式,熟悉下这两种算法可以帮助我们理解一些内存泄漏的场景。
引用计数垃圾收集
看下下面的例子,“这个对象”的内存被回收了吗?
// “这个对象”分配给 a 变量
var a = {
a: 1,
b: 2,
}
// b 引用“这个对象”
var b = a;
// 现在,“这个对象”的原始引用 a 被 b 替换了
a = 1;
当前执行环境中,“这个对象”内存还没有被回收的,需要手动释放“这个对象”的内存(当然是还没离开执行环境的情况下),例如:
b = null;
// 或者 b = 1,反正替换“这个对象”就行了
这样引用的"这个对象"的内存就被回收了。
ES6 把引用有区分为强引用和弱引用,这个目前只有再 Set 和 Map 中才有。
强引用才会有引用计数叠加,只有引用计数为 0 的对象的内存才会被回收,所以一般需要手动回收内存(手动回收的前提在于标记清除法还没执行,还处于当前执行环境)。
而弱引用没有触发引用计数叠加,只要引用计数为 0,弱引用就会自动消失,无需手动回收内存。
标记清除法
环境可以理解为我们的作用域,但是全局作用域的变量只会在页面关闭才会销毁。
// 假设这里是全局变量
// b 被标记进入环境
var b = 2;
function test() {
var a = 1;
// 函数执行时,a 被标记进入环境
return a + b;
}
// 函数执行结束,a 被标记离开环境,被回收
// 但是 b 就没有被标记离开环境
test();
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>
文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞?收藏加关注?,希望点赞多多多多...
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!