JavaScript系列之内存泄漏

栏目: JavaScript · 发布时间: 5年前

内容简介:在程序运行过程中不再用到的内存,没有及时释放,会出现内存泄漏(memory leak),会造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。而内存泄漏是每个开发人员最终必须面对的问题。 即使使用内存管理语言,比如C语言有着这很麻烦,所以为了编程中的负担,大多数语言提供了自动内存管理,这被称为"垃圾回收机制"(garbage collector)。

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

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

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

垃圾回收机制

现在各大浏览器通常采用的垃圾回收有两种方法: 标记清除(mark and sweep)引用计数(reference counting)

1、标记清除

这是javascript中最常用的垃圾回收方式。

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

工作流程:

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

2、引用计数

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

工作流程:

  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垃圾回收机制很快做出判断并且回收,但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量。

我们在使用闭包的时候,就会造成严重的内存泄漏,因为闭包的原因,局部变量会一直保存在内存中,所以在使用闭包的时候,要多加小心。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

图论——一个迷人的世界

图论——一个迷人的世界

本杰明,查特兰,张萍 / 机械工业出版社 / 2001-1-1

本书介绍了图论的基本概念,解释了图论中各种经典问题。例如,熄灯的问题、小生成树问题、哥尼斯堡七桥问题、中国邮递员问题、国际象棋中马的遍历问题和路的着色问题等等。书中也给出了各种类型的图,例如,二部图、欧拉图、彼得森图和树;等等。每一章都为读者设置了练习题,包含了具有挑战性的探索性问题。一起来看看 《图论——一个迷人的世界》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具