在程序运行过程中不再用到的内存,没有及时释放,会出现内存泄漏(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 内存泄漏及如何避免。目前水平有限,借鉴了国外大牛的文章了解这几种内存泄漏,原文链接:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec

四种常见的 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

https://github.com/miqilin21/miqilin21.github.io



JavaScript      JavaScript 内存

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!