这是一篇关于前端内存知识的博文,主要讲述了什么是内存?内存生命周期,内存泄漏,解决与避免方案。
什么是内存?
在硬件级别上,计算机内存由大量触发器组成。每个触发器包含几个晶体管,能够存储一个位。单个触发器可以通过唯一标识符寻址,因此我们可以读取和覆盖它们。因此,从概念上讲,我们可以把我们的整个计算机内存看作是一个巨大的位数组,我们可以读和写。
但是在JavaScript中我们无需在意内存的读写操作,因为这些Chrome都会帮我们处理好,我们需要做的就是避免变量或属性不能被正常回收即可。
生命周期
无论是什么程序语言,大家对内存的生命周期规定基本一致。
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还
分配 使用 释放
内存泄漏
现代的高级语言绝大部分都是具备自动GC的能力的,如果发生了内存的泄漏,最大可能是由于疏忽或错误造成程序未能释放已经不再使用的内存导致的。
如果内存不需要的时候,如果此时没有被释放,那么就会造成内存泄漏了。
简单总结一下:内存泄漏就是无用的变量占据着内存,无法释放归还内存。严重的时候甚至造成页面卡顿,崩溃。
内存回收
内存回收我们一般也称为垃圾回收即GC(Garbage Collection)。
内存泄漏一般都是发生在这一步,虽说现在的GC机制已经能回收绝大部分垃圾内存,但是还是存在回收不了的情况,此时我们就需要去手动清理。
下面说一下两种常见的GC算法
引用计数法
该算法是什么简单,如果当前对象没有被其他任何对象使用,那么此时该对象就能被回收。
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
该算法存在一个限制:无法理解循环引用,比如下面的例子。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
标记清除算法
通俗来讲就是对正在使用的变量进行标记,如果正在使用标记为1 未使用标记为0,GC在回收的时候发现该对象标记为0,那么就对该对象进行回收操作。
JavaScript的内存分配
像C语言这样的底层语言一般都有底层的内存管理接口,比如
malloc()
和free()
。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。
JavaScript不需要程序员手动分配内存,绝大多数情况下都能够自动完成内存的分配,使用,和回收等操作。
JavaScript对内存的管理机制是和内存的生命周期一一对应的,分配,使用,释放。
1.记住JavaScript 定义变量就会自动分配内存的。我们只需了解 JavaScript 的内存是自动分配的就足够了。
2.使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
内存泄漏场景
JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况。程序员要让浏览器内存泄漏,浏览器也是管不了的。
全局变量
// 在全局作用域下定义
function count(number) {
// basicCount 相当于 window.basicCount = 2;
basicCount = 2;
return basicCount + number;
}
这里就得突出eslint的重要性了。
被遗忘的计时器
新手常犯的错误之一。
mounted() {
setInterval(function() {
// 轮询获取数据
this.refresh()
}, 2000)
},
当一个计时器启动的时候,无论外部怎么销毁内部是无法清理的,我们需要调用专门的清除函数去手动清理。
mounted() {
this.refreshInterval = setInterval(function() {
// 轮询获取数据
this.refresh()
}, 2000)
},
beforeDestroy() {
clearInterval(this.refreshInterval)
},
上面就是一个比较合适的逻辑了clearInterval完成对setInterval的清理。
被遗忘的事件监听器
这也是作为内存泄漏的高频问题同样是需要用户去手动卸载的。
例如:
<template>
<div></div>
</template>
<script>
export default {
mounted() {
window.addEventListener('resize', () => {
// 这里做一些操作
})
},
}
</script>
组件销毁的时候,resize 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存)
这里我们需要window.removeEventListener(‘resize’, this.resizeEventCallback)去完成对Listener的卸载。
其他
Set,Map,订阅发布事件监听器,闭包,脱离 DOM 的引用等。
泄漏定位
1)通过google的开发者工具,切换到Performance选中Memory,点击录制。
如果内存平稳,不再递增,那么就是正常的,如果出现一直递增那么就有可能出现内存泄漏。
2)查找位置,利用Memory录制快照,查找Shallow Size比较大的进行分析。