美文网首页
Javascript内存泄漏和常见案例

Javascript内存泄漏和常见案例

作者: Jason_Shu | 来源:发表于2019-04-30 21:00 被阅读0次

    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内存泄漏和常见案例

          本文链接:https://www.haomeiwen.com/subject/zarbnqtx.html