1. 什么是Javascript内存泄漏?
我们知道程序的允许需要内存,只要程序提出要求,操作系统或者运行时就要提供内存。
内存泄漏并不是指内存在物理上的消失,而是「不再用到的内存,没有及时释放的内存」成为“内存泄漏”。
2. Javascript垃圾回收机制
垃圾回收机制怎么知道哪些内存不需要了呢?最常用的方法是「引用计数法」和「标记清除法」。
2.1 引用计数法
语言引擎有一张“引用表”,保存了内存里面所有的资源的「引用次数」。如果一个值的「引用次数」为0,表示这个值不再被用到,因此这块内存会被释放。
比如:
(1)声明一个变量a = {a: 1}
,这是{a: 1}
这个对象的「引用次数」就是1
(2)如果我们再让b = a
,也就是说变量b同样引用{a: 1}
这个对象,所以此时该对象的「引用次数」就是2
(3)相反,如果我们此时把变量b指向其他值或者设置为null,则会使得{a: 1}
这个对象的「引用次数」减去1.
let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1
let obj2 = obj1; // A 的引用个数变为 2
obj1 = 0; // A 的引用个数变为 1
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了
但是「引用计数法」有一个明显的缺点就是「循环引用」,下文中会提及。
2.2 标记清除法
(1)垃圾回收器会把所有在运行时存储在内存汇总的变量加一个"标记"。当变量进入环境时(比如在函数中声明一个变量),就将这个变量标记为「进入环境」,而当变量离开环境时,就将其标记为「离开环境」。
(2)然后垃圾回收器会去掉「环境中的变量」以及「环境中的变量引用的变量的标记」(闭包)。
(3)这些被完成之后,被加上标记的变量被视为「准备删除的变量」,原因是环境中的变量已经无法被访问到了。
(4)最后垃圾回收器完成清理工作,销毁那些带标记的值,并释放它们所占用的的内存空间。
function fn() {
let a = 1;
let b = 2;
// 函数执行时,a和b被标记为「进入环境」
}
fn(); // 函数执行结束后,a和b被标记「离开环境」,我们不能再访问到变量a和b,然后被回收
3. 常见的内存泄漏案例和防范
3.1 意外的全局变量
Javascript处理未定义的变量的方式比较宽松,未定义的变量会在「全局对象」中创建一个变量。浏览器中的「全局变量」就是window。
function foo() {
bar1 = '1'; // 等价于: window.bar1 = '1'
this.bar2 = '2'; // 等价于: window.bar2 = '2'
}
foo();
防范:
使用「use strict」严格模式,可以有效避免上述问题。
注意:
那些用来临时存储大量数据的变量,应该确保处理完毕后,将此变量设置为「null」或者重新赋值。
3.2 循环引用
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
func(); // undefined
执行完func方法后,返回「undefined」,然后整个函数以及内部的变量都应该被回收。但是根据「引用计数法」,此时obj1和obj2对象的「引用次数」并不是0,所以obj1和obj2对象并不会被回收。
要解决这个问题只能手动将它们置空。
obj1 = null;
obj2 = null;
3.3 被遗忘的计时器和回调函数
let someResource = getData();
setInterval(() => {
const node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
上述例子中,每隔一秒就将得到数据放到节点中。但是在setInterval结束之前,回调函数里面的变量「node」和「这个回调函数本身」都无法被回收。那什么时候才结束呢?就是当你调用「clearInterval」的时候了。
如果回调函数没做什么事情了,而且setInterval没有被clear,那么就会造成「内存泄漏」。不仅如此,如果回调函数没有被回收,那么回调函数内「依赖的变量」也无法被回收,比如上述上的「someResource」。setTimeout同理。
防范:
当不需要setTimeout和setInterval的时候,及时clear掉。
3.4 DOM泄漏
(1)没有清理的DOM元素的引用
var refA = document.getElementById('refA');
document.body.removeChild(refA);
// #refA不能回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
解决方法:refA = null;
(2)给DOM对象增加的属性是一个对象的引用
var MyObject = {};
document.getElementById('myDiv').myProp = MyObject;
解决方法:在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;
(3)DOM对象和JS对象相互引用
function Encapsulator(element) {
this.elementReference = element;
element.myProp = this;
}
new Encapsulator(document.getElementById('myDiv'));
解决方法: 在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;
(4)给DOM对象增加「attachEvent」或者「addEventListener」绑定事件
function doClick() {}
element.attachEvent("onclick", doClick);
element.addEventListener("click", doClick);
(5)从外到内执行「appendChild」,这是即使调用「removeChild」也无法释放
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
解决方法:从内到外「appendChild」
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);
3.5 闭包
function outer() {
const name = 'Jason';
return function inner(){
console.log(name);
}
}
let p = outer();
p();
我们来解读一下上例中的最后两句。
-
let p = out()
: 返回一个inner函数保存在变量p中,并且引用了外部函数outer作用域中的name变量,由于垃圾回收机制,outer函数执行完毕后,变量name不会被回收。 -
p()
:执行返回的inner函数,依然能访问到变量name,输出Jason。
在inner函数从outer函数中被返回后,inner函数的的作用域链被初始化为包含「outer函数的活动对象」和「全局变量对象」。这样inner函数就可以访问在outer函数中定义的所有变量和参数,更重要的是,outer函数执行完毕后,其活动对象也不会被销毁,因为inner函数的作用域链仍在引用这个活动对象,换句话说,outer函数执行完毕后,其执行环境的作用域链会销毁,但是其活动对象仍会留在内存中,直到inner函数被销毁。
同时总结下闭包的优缺点:
优点:
(1)可以让一个变量常驻内存(如果用多了就是缺点了)
(2)避免污染全局变量
(3)私有化变量缺点:
(1)因为闭包会携带包含它的函数的作用域,因为比其他函数占用更多的内存。
(2)引起内存泄漏
参考:
- http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
- http://www.fly63.com/article/detial/225?type=2
- https://segmentfault.com/a/1190000017105467
- https://juejin.im/post/5b684f30f265da0f9f4e87cf
- https://segmentfault.com/a/1190000002778015