- A+
要了解内存泄漏与内存溢出,首先需要了解内存是怎么分配的,故此,本文将按照以下几节阐述:
- 内存管理
- 垃圾回收·
- 内存泄漏
内存管理
JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让 JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。
内存生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放归还
在JavaScript中第一步和第三步都是隐含的
为了不让程序员费心分配内存,JavaScript 在定义变量(值的初始化)时就完成了内存分配。
内存分配
var n = 123; // 给数值变量分配内存 var s = "azerty"; // 给字符串分配内存 var o = { a: 1, b: null }; // 给对象及其包含的值分配内存 // 给数组及其包含的值分配内存(就像对象一样) var a = [1, null, "abra"]; function f(a){ return a + 2; } // 给函数(可调用的对象)分配内存 // 函数表达式也能分配一个对象 someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue'; }, false);
有些函数调用结果是分配对象内存:
var d = new Date(); // 分配一个 Date 对象 var e = document.createElement('div'); // 分配一个 DOM 元素
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
当分配的内存不再使用时释放它
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
垃圾回收
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个 Javascript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
引用计数垃圾回收
最初级的垃圾回收,此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。内存发生泄漏
标记清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进,并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义。
循环引用只要无法从根出发获得对象就会被清除
限制:无法从根对象查询到的对象都会被清除
内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。(程序某个未使用的变量或者方法,长期占用内存不会释放,导致内存堆积浪费)引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏
内存溢出:“内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。”(因为某些原因,程序使用的内存大于硬件提供的内存,导致内存超出了)
————————————————
内存泄漏可能的原因
一、全局变量引用
这种问题发生的频率一般较小, 我觉得应该没人把全局变量绑定到组件上,或者使用变量没有声明。
需要注意的是,要尽可能少地声明全局变量,因为GCRoots的原因,很容易会在查看引用链的时候链接到window上的属性,造成干扰。
二、定时器未清除
如果你不确定是不是定时器导致的问题,可以打开【开发者工具】,在Chrome中会有警告
所以养成好习惯,setTimeout使用一定要清除
const timer = setTimeOut(() => { this.resize() clearTimeOut(timer) }) 复制代码
setInterval也是一样的处理,不过不太建议在代码里使用setInterval,主要是不太好控制和错误捕获不到。
在平时开发时,可以留意下控制台里面Chrome发出的warning,去看看背后的原因是什么,一般和性能都是有关系的,这样会让你知道怎么样写代码对于浏览器来说是没有问题的。
三、事件未清除
这也是很常见的问题,养成好习惯。
// mounted window.addEventListener('resize', this.resize) // beforeDestory widnow.removeEventListener('resize', this.resize) 复制代码
这里需要注意最好不要用on的方式去绑定事件,覆盖事件以及解绑处理起来会很麻烦。
四、console
生成环境最好不要有console.log,如果有,最好是文字解释或者格式化后的JSON字符串,不要引用当前实例的数据。
五、未添加到Document的dom
function download(url) { const a = document.createElement('a') a.href = url a.click() } 如果不手动将a置为null,那么该dom会一直造成泄漏 复制代码
六、使用一些插件时,未销毁由该插件生成的dom
比较常用的如Sortable
const sortableInstance = Sortable.create(……) sortableInstance.destroy() 复制代码
另外我们自己写的一些函数,如果生成了dom,或者引用了dom,都要记得销毁。
let loading = this.$loading({ ……, target: this.$refs.table.el }) …… loading.close() loading = null
参考引用: