内存泄漏

1,446 阅读4分钟

JavaScript 的内存是自动非配的,内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏。 内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。

1. 内存回收方法(垃圾回收 GC)

1.1 引用计数垃圾收

如果一个对象指向它的引用数为 0,那么它就应该被“垃圾回收”了。

循环引用时就会出现无法回收的情况。

1.2 标记清除法

为了确定一个对象是否被需要,这个算法会确定对象是否可以访问。

可达的会被标记,不可达的则会被发现为垃圾,从而进行回收。即使循环引用也可释放。

2. 常见的内存泄漏情况

2.1 意外的全局变量

function foo(arg) {
bar = "some text"; // 等价于 window.bar = "some text";
}

下面代码也会创建意外的全局变量:

function foo() {
this.var1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)// rather than being undefined.
foo();

为防止上述两种创建意外全局变量的可能性,可以使用严格模式,在 JavaScript 代码开头添加“use strict”。严格模式中在上面两种情况下会抛出错误。

由于全局无法自动回收,除非将其赋值为 null 或重新进行分配。 那么在我们使用全局变量时要注意,尤其是用来临时存储和处理大量信息的全局变量非常值得关注。 如果你必须使用全局变量来存储大量数据,请确保在使用完后,对其赋值为 null 或重新分配。

2.2 闭包

//这里是没有内存泄漏的,因为name 变量是要用到的(非垃圾)。
function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()
// 这里调用了 reverseName
reverseName();
//这样是有内存泄漏的,name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。
name 不是必须的,但是还是占用了内存,也不可被回收。
function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()

2.3 被遗忘的定时器或回调

如下,定时器可能会引用不再需要的节点或数据,若 renderer 在将来被移除,整个定时器模块都不再被需要,但 interval handler 因为 interval 的存货,所以无法被回收(要停止 interval,才能回收)。因此 serverData 可能存储了大量数据,不能被回收。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

解决办法:在他们不再被需要的时候显示地删除它们(或让相关对象变为不可达)。

var serverData = loadData();
let interval = setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

// 清除定时器
clearInterval(interval);

2.4 被遗忘的事件监听

launch-button 元素销毁后,click 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件。

var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);

//不需要的时候显示删除他们:
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);

// 移除相关事件
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

2.5 被遗忘的 Set 成员

如下是有内存泄漏的(成员是引用类型的,即对象)

let map = new Set();
let value = { test: 22};
map.add(value);

value= null;

//需要删除这个成员
let map = new Set();
let value = { test: 22};
map.add(value);

map.delete(value);
value = null;

//可使用 WeakSet,它的成员是弱引用,内存回收不会考虑到这个引用是否存在。
let map = new WeakSet();
let value = { test: 22};
map.add(value);

value = null;

2.6 被遗忘的 Map 键名

如下是有内存泄漏的(键值是引用类型的,即对象)

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;

//需要改成这样,才没内存泄漏:
let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

map.delete(key);
key = null;

//有个更便捷的方式,使用 WeakMap,WeakMap 的键名是弱引用,内存回收不会考虑到这个引用是否存在。
let map = new WeakMap();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

key = null;

3 如何避免内存泄漏

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
  • 注意程序逻辑,避免“死循环”之类的 ;
  • 避免创建过多的对象,不用了的东西要及时归还。

参考: segmentfault.com/a/119000002…