常见的JavaScript内存泄漏有哪些

2019-06-10138次阅读javascript

垃圾自动回收机制的出现使编程更加的简单,使得我们不需要再去考虑内存分配和释放的问题,而是更加的专注在我们产品功能的实现上。但是我们还是需要花时间去了解下垃圾收集机制是怎么工作的,以便后面能够更好的进行我们应用的性能调优等。

程序的运行需要内存,对于持续进行的服务进程(daemon),必须及时释放内存,否则内存占用会越来越高,影响系统性能,直至进程崩溃。所以不再用到的内存没有及时释放,就叫做内存泄漏(memory leak)

 

引用

垃圾回收(Garbage Collection)算法主要依赖于引用的概念。在内存管理的环境中, 一个对象如果有访问另一个对象的权限(隐式或者显式), 叫做一个对象引用另一个对象。 例如: 一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

 

引用计数垃圾收集

这是最简单的垃圾收集算法,指如果没有引用指向该对象(零引用), 对象将被垃圾回收机制回收。 示例:

let arr = [1, 2, 3, 4];
arr = null; // [1,2,3,4]这时没有被引用, 会被自动回收


限制: 循环引用

在下面的例子中, 两个对象被创建并互相引用, 就造成了循环引用。引用计数算法考虑到它们互相都有至少一次引用, 所以它们不会被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 引用 o2
  o2.p = o1; // o2 引用 o1. 这里会形成一个循环引用
}
f();

实际例子:

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

在上面的例子里, myDivElement这个DOM元素里的circularReference属性引用了myDivElement, 造成了循环引用。IE6, 7使用引用计数方式对DOM对象进行垃圾回收, 该方式常常造成对象被循环引用时内存发生泄漏。现代浏览器通过使用标记-清除内存回收算法, 来解决这一问题。

 

标记-清除算法

从2012年起, 所有现代浏览器都使用了标记-清除内存回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进。这个算法假定设置一个叫做根root的对象(在Javascript里,根是全局对象)。定期的垃圾回收器将从根开始,找到所有可以获得的对象和所有不能获得的对象。

 

自动垃圾回收(Garbage Collection)的问题

尽管自动GC很方便, 但是我们不知道GC什么时候会进行。这意味着如果我们在使用过程中使用了大量的内存, 而GC没有运行的情况下, 或者GC无法回收这些内存的情况下, 程序就有可能假死, 这个就需要我们在程序中手动做一些操作来触发内存回收。

 

常见的内存泄露案例

1、意外的全局变量

function foo(arg) {
    bar = "some text";
}

上述范例中会把bar定义到全局对象中, 在浏览器中就是window上,即无意间创建了全局变量。全局变量只会在页面被关闭后才会销毁,所以这种写法就会造成内存泄露, 当然在这个例子中泄露的只是一个简单的字符串, 但是在实际的代码中, 往往情况会更加糟糕。

function foo() {
    this.var1 = "potential accidental global";
}
//Foo 被调用时, this 指向全局变量(window)
foo();

如果使用全局变量来暂存大量的数据时, 记得在使用后, 对其重新赋值为null。

2. 未销毁的定时器和回调函数

在很多库中, 如果使用了JavaScript设计模式之发布订阅模式, 都会提供回调方法, 来调用一些回调函数,要记得回收这些回调函数. 举一个setInterval的例子。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每5秒调用一次

如果后续renderer元素被移除, 整个定时器实际上没有任何作用。但如果你没有回收定时器, 整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的serverData变量引用也无法被回收。

3. 闭包

在JS开发中, 我们会经常用到闭包(一个内部函数有权访问包含其的外部函数中的变量)。像下面这种情况下, 闭包也会造成内存泄露。

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 对于 'originalThing'的引用
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

这段代码, 每次调用replaceThing时, theThing获得了包含一个巨大的数组和一个对于新闭包someMethod的对象。同时unused是一个引用了originalThing的闭包。

这个范例的关键在于, 闭包之间是共享作用域的, 尽管unused可能一直没有被调用, 但是someMethod可能会被调用, 就会导致内存无法对其进行回收。当这段代码被反复执行时, 内存会持续增长。该问题的更多描述可见Meteor团队的这篇文章

4. DOM引用

很多时候我们对Dom的操作, 会把Dom的引用保存在一个数组或者Map中。

var elements = {
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    document.body.removeChild(document.getElementById('image'));
    // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}

上述案例中, 即使我们对于image元素进行了移除, 但是仍然有对image元素的引用, 依然无法对齐进行内存回收。

另外需要注意的一个点是, 对于一个Dom树的叶子节点的引用。举个例子: 如果我们引用了一个表格中的td元素, 一旦在Dom中删除了整个表格, 我们直观的觉得内存回收应该回收除了被引用的td外的其他元素。但是事实上, 这个td元素是整个表格的一个子元素, 并保留对于其父元素的引用. 这就会导致对于整个表格, 都无法进行内存回收。所以我们要小心处理对于Dom元素的引用。

 

关于弱引用WeakSet和WeakMap

ES6中引入WeakSet和WeakMap两个新的概念, 来解决引用造成的内存回收问题。WeakSet和WeakMap对于值的引用可以忽略不计, 他们对于值的引用是弱(Weak)引用,内存回收机制, 不会考虑这种引用。当其他引用被消除后, 引用就会从内存中被释放。

const wm = new WeakMap();
const element = document.getElementById('example');
vm.set(element,'something');
vm.get(element);

上面代码中,先新建一个Weakmap实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在WeakMap里面。这时,WeakMap里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap保存的这个键值对,也会自动消失。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

上一篇: max-height实现任意高度元素的动画  下一篇: chrome浏览器下去掉文本框黄色背景及取消表单记忆功能  

常见的JavaScript内存泄漏有哪些相关文章