JavaScript系列以内存泄漏

在程序运行过程当中再也不用到的内存,没有及时释放,会出现内存泄漏(memory leak),会形成系统内存的浪费,致使程序运行速度减慢甚至系统崩溃等严重后果。javascript

而内存泄漏是每一个开发人员最终必须面对的问题。 即便使用内存管理语言,好比C语言有着malloc()free() 这种低级内存管理语言也有可能出现泄露内存的状况。html

这很麻烦,因此为了减轻编程中的负担,大多数语言提供了自动内存管理,这被称为"垃圾回收机制"(garbage collector)。java

垃圾回收机制

如今各大浏览器一般采用的垃圾回收有两种方法:标记清除(mark and sweep)引用计数(reference counting)node

一、标记清除git

这是javascript中最经常使用的垃圾回收方式。程序员

工做原理:当变量进入执行环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。github

工做流程:算法

  1. 垃圾回收器,在运行的时候会给存储在内存中的全部变量都加上标记。
  2. 去掉环境中的变量以及被环境中的变量引用的变量的标记。
  3. 以后再被加上标记的变量将被视为准备删除的变量。
  4. 垃圾回收器完成内存清除工做,销毁那些带标记的值并回收他们所占用的内存空间。

二、引用计数编程

工做原理:跟踪记录每一个值被引用的次数。数组

工做流程:

  1. 将一个引用类型的值赋值给这个声明了的变量,这个引用类型值的引用次数就是1。
  2. 同一个值又被赋值给另外一个变量,这个引用类型值的引用次数加1。
  3. 当包含这个引用类型值的变量又被赋值成另外一个值了,那么这个引用类型值的引用次数减1
  4. 当引用次数变成0时,就表示这个值再也不用到了。
  5. 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。

但若是一个值再也不须要了,引用数却不为0,垃圾回收机制没法释放这块内存,会致使内存泄漏。

var arr = [1, 2, 3];
console.log('hello miqilin');
复制代码

上面代码中,数组[1, 2, 3]会占用内存,赋值给了变量arr,所以引用次数为1。尽管后面的一段代码没有用到arr,它仍是会持续占用内存。

若是增长一行代码,解除arr对[1, 2, 3]引用,这块内存就能够被垃圾回收机制释放了。

var arr = [1, 2, 3];
console.log('hello miqilin');
arr = null;
复制代码

上面代码中,arr重置为null,就解除了对[1, 2, 3]的引用,引用次数变成了0,内存就能够释放出来了。

所以,并非说有了垃圾回收机制,程序员就无事一身轻了。你仍是须要关注内存占用:那些很占空间的值,一旦再也不用到,你必须检查是否还存在对它们的引用。若是是的话,就必须手动解除引用。

接下来,我将介绍四种常见的JavaScript 内存泄漏及如何避免。目前水平有限,借鉴了国外大牛的文章了解这几种内存泄漏,原文连接:blog.sessionstack.com/how-javascr…

四种常见的 JavaScript 内存泄漏

1.意外的全局变量

未定义的变量会在全局对象建立一个新变量,对于在浏览器的状况下,全局对象是window。 看如下代码:

function foo(arg) {
     bar = "this is a hidden global variable"; 
}
复制代码

函数foo内部使用var声明,实际上JS会把bar挂载在全局对象上,意外建立一个全局变量。等同于:

function foo(arg) {
     window.bar = "this is an explicit global variable"; 
}
复制代码

在上述状况下, 泄漏一个简单的字符串不会形成太大的伤害,但它确定会更糟。

另外一种能够建立偶然全局变量的状况是this

function foo() {
     this.variable = "potential accidental global"; 
}  
// Foo called on its own, this points to the global object (window)
// rather than being undefined. 
foo();
复制代码

解决方法:

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

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

在JavaScript中使用setInterval很是常见。

var someResource = getData(); 
setInterval(function() {
     var node = document.getElementById('Node');     
     if(node) {
         // Do stuff with node and someResource.
         node.innerHTML = JSON.stringify(someResource));
     } }, 1000);
复制代码

上面的代码代表,在节点node或者数据再也不须要时,定时器依旧指向这些数据。因此哪怕当node节点被移除后,interval 仍旧存活而且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。

var element = document.getElementById('button');  

function onClick(event) {
     element.innerHtml = 'text'; 
}  

element.addEventListener('click', onClick); // Do stuff 
element.removeEventListener('click', onClick); 
element.parentNode.removeChild(element); 

// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
复制代码

对于上面观察者的例子,一旦它们再也不须要(或者关联的对象变成不可达),明确地移除它们很是重要。其中IE 6 是没法处理循环引用的。由于老版本的 IE 是没法检测 DOM 节点与 JavaScript 代码之间的循环引用,会致使内存泄漏。

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

诸如jQuery之类的框架和库在处理节点以前会删除侦听器(当使用它们的特定API时)。 这由库内部处理,并确保不会产生任何泄漏,即便在有问题的浏览器(如旧版Internet Explorer)下运行也是如此。

3.闭包

JavaScript 开发的一个关键知识是闭包:这是一个内部函数,它能够访问外部(封闭)函数的变量。因为 JavaScript 运行时的实现细节,用下边这种方式可能会形成内存泄漏:

var theThing = null; 
var replaceThing = function () {
   var originalThing = theThing;   
   var unused = function () {
     if (originalThing)
       console.log("hi");   
};   
   theThing = {
     longStr: newArray(1000000).join('*'),
     someMethod: function () {
       console.log(someMessage);
     }
   };
 }; 
setInterval(replaceThing, 1000);
复制代码

每次调用replaceThingtheThing获得一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unused是一个引用originalThing的闭包(先前的replaceThing又调用了theThing)。someMethod能够经过theThing使用,someMethodunused分享闭包做用域,尽管unused从未使用,它引用的originalThing迫使它保留在内存中(防止被回收)。须要记住的是一旦一个闭包做用域被同一个父做用域的闭包所建立,那么这个做用域是共享的

全部这些均可能致使严重的内存泄漏。当上面的代码片断一次又一次地运行时,你能够看到内存使用量的急剧增长。当垃圾收集器运行时,也不会减小。一个连接列表闭包被建立(在这种状况下 theThing 变量是根源),每个闭包做用域对打数组进行间接引用。

解决方法:

replaceThing 的最后添加 originalThing = null 。将全部联系都切断。

4.脱离 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);
     // Much more logic
} 
 
function removeButton() {
     // The button is a direct child of body.
     document.body.removeChild(document.getElementById('button'));

    // At this point, we still have a reference to #button in the global
    // elements dictionary. In other words, the button element is still in
    // memory and cannot be collected by the GC. 
}
复制代码

若是代码中保存了表格某一个<td>的引用。未来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的<td>之外的其它节点。实际状况并不是如此:此<td>是表格的子节点,子元素与父元素是引用关系。因为代码保留了<td>的引用,致使整个表格仍待在内存中。因此保存 DOM 元素引用的时候,要当心谨慎。

避免内存泄漏

在局部做用域中,等函数执行完毕,变量就没有存在的必要了,js垃圾回收机制很快作出判断而且回收,可是全局变量何时须要自动释放内存空间则很难判断,所以在咱们的开发中,须要尽可能避免使用全局变量。

咱们在使用闭包的时候,就会形成严重的内存泄漏,由于闭包的缘由,局部变量会一直保存在内存中,因此在使用闭包的时候,要多加当心。

Resources

若是有别的关于内存泄漏好的资源,能够分享给我嘛谢谢了~

本人Github连接以下,欢迎各位Star

github.com/miqilin21/m…

相关文章
相关标签/搜索