JavaScript系列之内存泄漏
栏目: JavaScript · 发布时间: 5年前
内容简介:在程序运行过程中不再用到的内存,没有及时释放,会出现内存泄漏(memory leak),会造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。而内存泄漏是每个开发人员最终必须面对的问题。 即使使用内存管理语言,比如C语言有着这很麻烦,所以为了编程中的负担,大多数语言提供了自动内存管理,这被称为"垃圾回收机制"(garbage collector)。
在程序运行过程中不再用到的内存,没有及时释放,会出现内存泄漏(memory leak),会造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
而内存泄漏是每个开发人员最终必须面对的问题。 即使使用内存管理语言,比如 C语言 有着 malloc()
和 free()
这种低级内存管理语言也有可能出现泄露内存的情况。
这很麻烦,所以为了编程中的负担,大多数语言提供了自动内存管理,这被称为"垃圾回收机制"(garbage collector)。
垃圾回收机制
现在各大浏览器通常采用的垃圾回收有两种方法: 标记清除(mark and sweep) 、 引用计数(reference counting) 。
1、标记清除
这是javascript中最常用的垃圾回收方式。
工作原理:当变量进入执行环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记。
- 之后再被加上标记的变量将被视为准备删除的变量。
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
2、引用计数
工作原理:跟踪记录每个值被引用的次数。
工作流程:
- 将一个引用类型的值赋值给这个声明了的变量,这个引用类型值的引用次数就是1。
- 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1。
- 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减1
- 当引用次数变成0时,就表示这个值不再用到了。
- 当垃圾收集器下一次运行时,它就会释放引用次数是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); 复制代码
每次调用 replaceThing
, theThing
得到一个包含一个大数组和一个新闭包( someMethod
)的新对象。同时,变量 unused
是一个引用 originalThing
的闭包(先前的 replaceThing
又调用了 theThing
)。 someMethod
可以通过 theThing
使用, someMethod
与 unused
分享闭包作用域,尽管 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
本书介绍了图论的基本概念,解释了图论中各种经典问题。例如,熄灯的问题、小生成树问题、哥尼斯堡七桥问题、中国邮递员问题、国际象棋中马的遍历问题和路的着色问题等等。书中也给出了各种类型的图,例如,二部图、欧拉图、彼得森图和树;等等。每一章都为读者设置了练习题,包含了具有挑战性的探索性问题。一起来看看 《图论——一个迷人的世界》 这本书的介绍吧!