Javascript内存泄漏和常见案例

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)引起内存泄漏

参考:

  1. http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
  2. http://www.fly63.com/article/detial/225?type=2
  3. https://segmentfault.com/a/1190000017105467
  4. https://juejin.im/post/5b684f30f265da0f9f4e87cf
  5. https://segmentfault.com/a/1190000002778015

你可能感兴趣的:(Javascript内存泄漏和常见案例)