如何处理4种常见的内存泄露

如何处理4种常见的内存泄露

什么是内存泄露

本质上来说,内存泄露可以被定义为 当内存不再被应用使用,却因为某些原因没有被操作系统回收或释放到自由内存池

https://cdn-images-1.medium.com/max/1600/1*0B-dAUOH7NrcCDP6GhKHQw.jpeg

编程语言支持多种不同的内存管理。某些内存是否在使用实际上是不可判定的。换句话说,只要开发者才清楚哪些内存块是可以被操作系统回收。

某些编程语言提供一些特性帮助开发者进行内存回收。其他的期望开发者完全明白哪些内存是无用的。维基有一系列关于内存管理的好文章。

四种常见的内存泄露

1. 全局变量

javascript 通过一个有趣的方式处理未声明的变量:引用未声明的变量会在全局对象里创建一个变量。在浏览器中,全局对象是window, 换句话说:

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

等价于:

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

如果 bar 预想中是 foo 函数作用域内的局部变量,但又忘记使用 var 去声明,那么预料之外的全局变量就被创建了。

在这个例子中,一个简单的字符串泄露不会有多大害处,但这种写法当然会有更糟情况。

另一个意外创建全局变量的方式是通过 this

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

为了防止这种情况发生,需要在 javascript 文件的开头添加 'use strict'.这会使用严格模式解析javascript以防止意外的全局变量。了解更多javascript严格模式的执行情况

尽管我们在谈论意外的全局变量,但仍然会有无数的明确的全局变量。这些被定义为不可回收的(除非指定为null或重新分配)。特别是应该关注用于临时存储和处理大量数据的全局变量。如果必须要用全局变量去存储大量数据,当你处理完之后,请勿必记得指定为null或重新全配

2. 被遗忘的定时器和回调

在 javascript 中使用 setInterval 非常常见。

大部分类库提供观察者和其他调用回调的工具,小心地引用那些 拥有会变得不可访问的实例 的回调。setInterval 的例子,尽管这很常见:

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

这个例子说明了定时器会发生的事:定时器引用了不再需要的node节点和数据。

renderer 引用的对象在将来某个时刻可能会被删除,让整个内部块都变得不必要了。但是,因为定时器还在运行,所以处理程序不能被回收(定时器需要被停止才能回收)。如果定时处理程序不能被回收,那么相关的一切都不能被回收。这意味着可能存储着大量数据的 serverData 也不能被回收。

观察者的例子中,很重要的一点是当不在需要的时候,必须明确移除他们(或者让相关的对象变得不可访问)。

在过去,这对于某些不能很好地管理内存的浏览器(像IE6)是非常重要的。如今,大部分浏览器有能力并且会回收不可访问的观察者处理程序,就算有时监听器没有被明确地移除。这仍然是很好的做法,不管怎样,在对象被处理掉之前明确地移除观察者。举个实例:

var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
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.

框架或类库,比如 jQuery 在删除 node 节点的时候就移除了监听事件(当使用它的API的时候),这是类库内部处理的,为了防止内存泄露产生。就算在有问题的浏览器下运行,比如…对,IE6 :)

闭包(closure)

javascript 开发一个关键点是 闭包: 一个内部函数可以访问(封闭的)外部函数的变量。由于 javascript 运行环境的实现,以下方式很可能造成内存泄露:

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

这段代码做了一件事:每次调用 replaceThing 时,theThing 指向了一个包含大数据和闭包(someMethod)的新对象,同时, unused 也指向一个引用了 originalThingtheThing 指向前一次 replaceThing 调用) 的闭包,已经有点混乱了,啊哈?重点是,一旦为一个具有相同父级作用域的闭包创建作用域,作用域是共享的

这种情况下, 为闭包 someMethod 创建的作用域与 unused 共享。unused 有一个指向 originalThing 的引用。尽管 unused 从未被使用,但是 someMethod 可以通过 theThing 也就是 replaceThing 外围的作用域(例如:全局范围内)调用。与此同时 someMethodunused 共享闭包作用域,unused 引用 originalThing 迫使它保持在内存中(在两个闭包共享的作用域范围)。这阻止了它被回收。

当这段代码反复地运行时,可以观察到内存使用率稳定地增长。GC 运行时也并未减少。本质上,一个闭包链被创建了(通过根作用域的 theThing 变量)。而每一个闭包作用域附带着引用了大数据,结果就是内存泄露。

这个问题被 Meteor 团队发现并发表了著名的文章描述这个问题

4. 额外的 DOM 引用

有时在数据结构中引用 DOM 节点很有用。假设你想迅速地更新一个表格中的几行内容。可能感觉在字典中或数据中引用 DOM row 是有意义的。在这种情况发生时,两个对相同 DOM 的引用被保持:一个在 DOM 树,另一个在字典中。如果将来某个时间点,你想删除这几行表格,你要确保这两个者失去引用。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
function doStuff() {
    image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    // The image is a direct child of the body element.
    document.body.removeChild(document.getElementById('image'));
    // At this point, we still have a reference to #button in the
    //global elements object. In other words, the button element is
    //still in memory and cannot be collected by the GC.
}

这里还有额外的点需要注意,当涉及到 DOM 树或子节点的引用。比如说在 javascript 代码中引用了表格的某个格子(一个标签)。某天你决定删除表格但还保持着单元格的引用。直观的假设就是 GC 会回收表格,但保留单元格。事实上,这是不可能的:单元格是表格的子节点,它保持着对父级表格的引用。javascript 代码保持单元格的引用导致 整个表格被保留在内存中。所以请谨慎考虑引用 DOM 元素。

0%