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 如何避免内存泄漏
- 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
- 注意程序逻辑,避免“死循环”之类的 ;
- 避免创建过多的对象,不用了的东西要及时归还。