一、内存泄露
系统进程不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
二、引起内存泄露的原因
1、意外的全局变量
由于 js 对未声明变量的处理方式是在window对象上创建该变量的引用。
变量在窗口关闭或重新刷新页面之前都不会被释放
如果未声明的变量缓存大量的数据,就会导致内存泄露
// 未声明的变量
function fn() {
a = 'global variable'
}
fn()
// 使用 this 创建的变量(this 的指向是 window)。
function fn() {
this.a = 'global variable'
}
fn()
解决方法:
- 避免创建全局变量
- 使用严格模式, 文件头部或者函数的顶部加上
use strict
2、闭包引起的内存泄漏
原因:闭包可以读取函数内部的变量,然后让这些变量始终保存在内存中。
如果在使用结束后没有将局部变量清除,就可能导致内存泄露。
function fn () {
var a = "I'm a";
return function () {
console.log(a);
};
}
解决方法:
将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中。
比如:在循环中的函数表达式,能复用最好放到循环外面。
// bad
for (var k = 0; k < 10; k++) {
var t = function (a) {
// 创建了10次 函数对象。
console.log(a)
}
t(k)
}
// good
function t(a) {
console.log(a)
}
for (var k = 0; k < 10; k++) {
t(k)
}
t = null
3、没有清理的 DOM 元素引用
原因:虽然别的地方删除了,但是对象中还存在对 dom 的引用。
// 在对象中引用DOM
var elements = {
btn: document.getElementById('btn'),
}
function doSomeThing() {
elements.btn.click()
}
function removeBtn() {
// 将body中的btn移除, 也就是移除 DOM树中的btn
document.body.removeChild(document.getElementById('button'))
// 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收
}
解决方法:
手动删除,elements.btn = null。
4、被遗忘的定时器或者回调
定时器中有 dom 的引用,即使 dom 删除了,但是定时器还在,所以内存中还是有这个 dom。
// 定时器
var serverData = loadData()
setInterval(function () {
var renderer = document.getElementById('renderer')
if (renderer) {
renderer.innerHTML = JSON.stringify(serverData)
}
}, 5000)
// 观察者模式
var btn = document.getElementById('btn')
function onClick(element) {
element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick)
计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,serverData 如果存储了大量的数据,也是无法被回收的。
对于观察者模式,老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。
解决方法:
- 手动删除定时器和 dom。
- removeEventListener 移除事件监听
三、内存分配和垃圾回收机制
1. 内存分配
1.1 值的初始化
为了不让程序员费心分配内存,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);
1.2 通过函数调用分配内存
// 有些函数调用结果是分配对象内存:
var d = new Date(); // 分配一个 Date 对象
var e = document.createElement('div'); // 分配一个 DOM 元素
// 有些方法分配新变量或者新对象:
var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
// 新数组有四个元素,是 a 连接 a2 的结果
2. 垃圾回收机制
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
2.1. 引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
2.1.1 引用计数垃圾收集
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
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属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
2.1.2 限制:循环引用
该算法有个限制:无法处理循环引用的事例。
在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
IE 6, 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 属性),而这个数据占用的内存将永远不会被释放。
2. 标记-清除算法
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
- 循环引用不再是问题了
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。 - 限制: 那些无法从根对象查询到的对象都将被清除
尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。
四、ES6防止内存泄露
及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。
ES6 考虑到这点,推出了两种新的数据结构: weakset 和 weakmap 。他们对值的引用都是不计入垃圾回收机制的,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。
const wm = new WeakMap()
const element = document.getElementById('example')
vm.set(element, 'something')
vm.get(element)
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对 element 的引用就是弱引用,不会被计入垃圾回收机制。
// 代码1
ele.addEventListener('click', handler, false)
// 代码2
const listener = new WeakMap()
listener.set(ele, handler)
ele.addEventListener('click', listener.get(ele), false)
代码 2 比起代码 1 的好处是:由于监听函数是放在 WeakMap 里面,一旦 dom 对象 ele 消失,与它绑定的监听函数 handler 也会自动消失
五、map set weakMap weakSet区别
1. Map
对象:无序键值对,键必须是字符串
map: key可以是任何类型
const a = new Map([['a',1],['b',2]])
const x = {id:1}
const y= {id:2}
const b ={}
b[x] = 'bx'
b[y] = 'by'
console.log(b[x]) // by
console.log(b[y]) // by
c=new Map();
c.set(x, 'cx')
c.set(y, 'cy')
console.log(c.get(x)) // cx
console.log(c.get(y)) // cy
console.log(JSON.stringify([...c.entries()],null,' '))
[
[
{
"id": 1
},
"cx"
],
[
{
"id": 2
},
"cy"
]
]
console.log(JSON.stringify([...c.values()],null,' '))
[
"cx",
"cy"
]
console.log(JSON.stringify([...c.keys()],null,' '))
[
{
"id": 1
},
{
"id": 2
}
]
2. weakMap
weakMap只接受对象为键,这些对象是弱持有的,如果这个对象对垃圾回收,weakMap中这个键值对也会消失
weakMap没有size和clear,也不会暴露任何键、值或迭代器
c=new WeakMap();
c.set(x, 'cx')
c.set(y, 'cy')
x= null // 可回收
y= null // 可回收
3.Set
const s = new Set()
var x={id:1}
var y={id:2}
s.add(x)
s.add(y)
console.log(s.size)
s.delete(x)
s.clear()
s.has(x)
4. WeakSet
weakSet弱持有,值只能是对象,同样,对象可回收