常见的内存泄漏及如何避免

933 阅读4分钟

内存泄漏: 对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。对于不再用到的内存,没有及时释放,就叫做内存泄漏。

内存泄漏识别方法:

  • 浏览器方法:
  1. 打开开发者工具,选择Memory
  2. 在右侧的Select profiling type字段里面勾选timeline
  3. 点击左上角的录制按钮
  4. 在页面上进行各种操作,模拟用户的使用情况
  5. 一段时间后,点击左上角的stop按钮,面板上就会显示这段时间的内存占用情况。
  • 命令行方法 使用Node提供的process.memoryUsage方法。
console.log(process.memoryUsage()); 
//输出 
{ 
    rss:27709440, //resident set size,所有内存占用,包括指令区和堆栈 
    heapTotal:5685248, //"堆"占用的内存,包括用到的和没用到的 
    heapUsed:3449392, //用到的堆的部分 
    external:8772 //V8引擎内部的C++对象占用的内存 
}

判断内存泄漏,以heapUsed字段为准。

四种常见的JS内存泄漏(划重点)

  1. 意外的全局变量 未定义的变量会在全局对象创建一个新变量
function foo(arg){ 
    a = "this is a hidden global variable"; 
}

函数foo内部忘记使用var,实际上JS会把a挂载到全局对象上,意外创建一个全局变量。

function foo(arg){ 
    window.a = "this is an explicit global variable"; 
}

另一个意外的全局变量可能有this创建。

function foo(){ 
    this.variable = "potential accidental global"; 
} 
//foo 调用自己,this指向了全局对象window 
//而不是undefined foo()

解决方法:

在JS文件头部加上'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为null或者重新定义。

  1. 被遗忘的计时器或回调函数

    计时器setInterval代码很常见

var someResource = getData(); 
    setInterval(function(){ 
    var node = document.getElementById('Node'); 
    if(node){ 
        //处理node和someResource 
        node.innerHtml = JSON.stringify(someResource); 
    } 
},1000);

上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,setInterval仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法回收,除非终止定时器。

var element = document.getElementById('button'); 
function onClick(event){ 
    element.innerHTML = 'text'; 
} 
element.addEventListener('click',onClick);

对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的IE6是无法处理循环引用的。因为老版本的IE是无法检测DOM节点与JS代码之间的循环引用,会导致内存泄漏。

但是,现代的浏览器(包括IE和Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用removeEventListener了。

  1. 脱离DOM的引用 如果把DOM存成字典(JSON键值对)或者数组,此时,同样的DOM元素存在两个引用:一个在DOM树中,另一个在字典中。那么将来需要把两个引用都清除。
var elements = { 
    button: document.getElementById('button'), 
    image: document.getElementById('image'), 
    text: document.getElementById('text') 
}; 
function doStuff() { 
    image.src = 'http://some.url/image'; 
    button.click(); 
    console.log(text.innerHTML); 
    // 更多逻辑 
} 
function removeButton() { 
    // 按钮是 body 的后代元素 
    document.body.removeChild(document.getElementById('button')); 
    // 此时,仍旧存在一个全局的 #button 的引用 
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。 
}

如果代码中保存了表格某一个的引用。将来决定删除整个表格的时候,直觉认为GC会回收除了已保存的以外的其它节点。实际情况并非如此:此是表格的子节点,子元素和父元素是引用关系。由于代码保留了的引用,导致了整个表格仍待在内存中。所以保存DOM元素引用的时候,要小心谨慎。

  1. 闭包 闭包的关键时匿名函数可以访问父级作用域的变量。
var theThing = null; 
var replaceThing = function(){ 
    var originalThing = theThing; 
    var unused = function(){ 
        if(originalThing) 
        console.log('hi'); 
    }; 
    theThing = { 
        longStr:new Array(1000000).join('*'), 
        someMethod:function(){ 
            console.log(someMessage); 
        } 
    }; 
}; 
setInterval(replaceThing,1000);

每次调用replaceThing,theThing得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unused是一个引用

originalThing的闭包(先前的replaceThing又调用了theThing)。someMethod可以通过theThing使用,someMethod与unused分享闭包作用域,尽管unused从未使用,它引用的originalThing迫使它保留在内存中(防止被回收)。

解决方法: 在 replaceThing 的最后添加 originalThing = null 。