一,内存生命周期:【不管什么语言,内存生命周期都是一致的】
js环境中分配的内存有以下生命周期
分配:分配所需的内存
使用:使用分配的内存(读/写)
回收:不需要时将其归还
1.js的内存分配
- 为了不让程序员费心分配内存,js在定义变量时就完成了内存分配,如下:
let n = 123;//给数值变量分配内存
let s = "abc"; // 给字符串分配内存
let o = { a: 1, b: null, };
// 给对象及其包含的值分配内存
// 给数组及其包含的值分配内存(就像对象一样)
let a = [123, null, "abc"];
function f(a) { return a + 2; } // 给函数(可调用的对象)分配内存
// 函数表达式也能分配一个对象
someElement.addEventListener( "click", function () {
someElement.style.backgroundColor = "blue";
}, false, );
- 通过函数调用分配内存
let data = new Date()
let e = document.createElement("div"); // 分配一个 DOM 元素
- 方法分配新变量或新对象
let s = "abc";
let s2 = s.substr(0, 3); // s2 是一个新的字符串
2.使用值
使用值的过程实际上是对分配内存进行读取与写入的操作;
3.当内存不再需要使用时释放
大多内存管理的问题都在这个阶段,如何找到“那些被分配的内存确实已经不再需要了”。它往往需要开发人员来确定在程序中哪一块不再需要且释放它
高级语言解释器嵌入了“垃圾回收器”,它的只要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只是一个近乎的过程,要知道是否仍然需要某块内存是无法判断的(无法通过某种算法解决)
二,垃圾回收机制
从内存生命周期中可以知道内存是否需要是无法判断的,因此垃圾回收机制只能有限的解决一般问题。
1.垃圾回收算法
1.引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(显式/隐式),叫做一个对象引用另一个对象。
如:一个js对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)
2.引用计数垃圾收集
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
函数被调用后,函数中的变量会离开函数作用域【函数中没有再引用】,可以被回收;
var o = {
a: {
b: 2,
},
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2 变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有
var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收
oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
3.限制:循环引用
如下:函数中的两个对象,相互引用,形成循环;它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f() {
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
2. 实例
- IE6,7使用引用计数的方式对DOM对象进行垃圾回收,该方法常常造成对象被循环引用时内存发生泄漏
var div;
window.onload = function () {
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从 DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放
- 标记清除算法 把“对象是否不再需要”简化定义为“对象是否可以获得”
这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进,并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义。
- 循环引用不再是问题了
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
三,内存泄漏
Q:什么是内存泄漏?
申请的内存不再使用后没有及时回收掉
Q:为什么会发生内存泄漏
前面说到,前端是有垃圾回收机制的,但是当某块无用的内存,无法被垃圾回收机制识别时,就发生内存泄漏了;
而垃圾回收机制通常是使用标记清除策略,发生内存泄漏的根本原因是,垃圾回收器无法识别,直接原因是,当不同生命周期的两个东西相互通信时,一方生命周期到期该回收了,却被另一方持有,也就发生内存泄漏了;
四,引起内存泄漏的场景
1.意外的全局变量
全局变量的生命周期最长,直到页面关闭前,它都是存活的,所以全局变量上的内存一直都不会被回收
当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了
2.遗忘的定时器
setTimeout 和 setInterval 是由浏览器专门线程来维护它的生命周期,所以当在某个页面使用了定时器,当该页面销毁时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的
也就是说,定时器的生命周期并不挂靠在页面上,所以当在当前页面的 js 里通过定时器注册了某个回调函数,而该回调函数内又持有当前页面某个变量或某些 DOM 元素时,就会导致即使页面销毁了,由于定时器持有该页面部分引用而造成页面无法正常被回收,从而导致内存泄漏了
如果此时再次打开同个页面,内存中其实是有双份页面数据的,如果多次关闭、打开,那么内存泄漏会越来越严重
而且这种场景很容易出现,因为使用定时器的人很容易遗忘清除
3.使用不当的闭包
函数本身会持有它定义时所在的词法环境的引用,但通常情况下,使用完函数后,该函数所申请的内存都会被回收了
但当函数内再返回一个函数时,由于返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期东西所持有,导致外部函数虽然执行完了,但内存却无法被回收
所以,返回的函数,它的生命周期应尽量不宜过长,方便该闭包能够及时被回收
正常来说,闭包并不是内存泄漏,因为这种持有外部函数词法环境本就是闭包的特性,就是为了让这块内存不被回收,因为可能在未来还需要用到,但这无疑会造成内存的消耗,所以,不宜烂用就是了
4.遗漏的DOM元素
DOM 元素的生命周期正常是取决于是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了
但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它
5.网络回调
某些场景中,在某个页面发起网络请求,并注册一个回调,且回调函数内持有该页面某些内容,那么,当该页面销毁时,应该注销网络的回调,否则,因为网络持有页面部分内容,也会导致页面部分内容无法被回收