1

一、内存泄露

系统进程不再用到的内存,没有及时释放,就叫做内存泄漏(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弱持有,值只能是对象,同样,对象可回收

参考

万恶的前端内存泄漏及万善的解决方案
内存管理
阮一峰内存泄露
JavaScript 内存泄露的4种方式及如何避免


lihaixing
463 声望719 粉丝

前端就爱瞎折腾