关于内存泄漏

Posted jodniki

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于内存泄漏相关的知识,希望对你有一定的参考价值。

概述

像 C 语言,拥有底层原始的内存管理方法,例如:malloc() 和 free()。这些原始的方法被开发者用来从操作系统中分配内存和释放内存。

然而,javascript 当一些东西(objects,strings,etc.)被创建的时候分配内存并且当它们不再被使用的时候“自动”释放它们,这个过程被称为垃圾回收。

释放资源的这种看似“自动”的性质是造成困扰的根源。它给 JavaScript (和其它高级语言)开发者一个错误的印象——他们可以选择不关心内存管理。这是一个很大的错误。

即使是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便能够正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)

 

关于内存

 

很多东西都存储在内存中:

 

  1. 所有程序使用的所有变量和其他数据。
  2. 程序的代码,包括操作系统的。

编译代码时,编译器可以检查原始数据类型,并提前计算出需要多少内存。然后将所需的数量分配给调用堆栈中的程序。这些变量分配的空间称为堆栈空间,因为随着函数被调用,它们的内存被添加到现有存储器的顶部。当它们终止时,它们以 LIFO (last-in,first-out)顺序被移除。静态和动态分配内存

它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地要求操作系统获得适当的空间量。这个内存时从堆空间分配的。静态和动态内存分配的区别如下表所示:

Static allocationDynamic allocation
编译时内存大小确定 编译时内存大小不确定
编译阶段执行 运行时执行
分配给栈 分配给堆
FILO 没有特定的顺序

JavaScript 中使用内存

基本上在 JavaScript 中使用内存的意思就是在内存在进行 读 和 写。

这个操作可能是一个变量值的读取或写入,一个对象属性的读取或写入,甚至时向函数中传递参数。

什么是内存泄漏

实质上,内存泄漏可以被定义为应用程序不再需要的内存,但由于某种原因,内存不会返回到操作系统或可用内存池中。

编程语言支持多种管理内存的方法。然而,某块内存是否被使用实际上是一个不确定的问题。换句话说,只有开发人员可以清楚一块内存是否可以释放到操作系统又或者不该被释放。

某些编程语言提供了一些特性,帮助开发者处理这些事情。另外一些语言期望开发者能够完全自己去明确地控制内存。维基百科有关于手动和自动内存管理的好文章。

四种常见的 JavaScript 内存泄漏

1:Global variables

JavaScript 以有趣的方式处理未声明的变量:对未声明的变量的引用在全局对象内创建一个新变量。在浏览器中,全局对象就是 window。换种说法:

  1.  
    function foo(arg) {
  2.  
    bar = "some text";
  3.  
    }

等价于:

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

如果 bar 被假定为仅仅在函数 foo 的作用域范围内持有对变量的引用,但是你却忘记了使用 var 来声明它,那么就会创建一个意外的全局变量。

在这个例子中,泄漏一个简单的字符串不会有太大的伤害,但肯定会变得更糟的。

可以通过另一种方式创建意外的全局变量:

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

为了防止这些错误的发生,可以在 JavaScript 文件开头添加 “use strict”,使用严格模式。这样在严格模式下解析 JavaScript 可以防止意外的全局变量。

即使我们讨论了如何预防意外全局变量的产生,但是仍然会有很多代码用显示的方式去使用全局变量。这些全局变量是无法进行垃圾回收的(除非将它们赋值为 null 或重新进行分配)。特别是用来临时存储和处理大量信息的全局变量非常值得关注。如果你必须使用全局变量来存储大量数据,那么,请确保在使用完之后,对其赋值为 null 或者重新分配。

2:被忘记的 Timers 或者 callbacks

在 JavaScript 中使用 setInterval 非常常见。

大多数库都会提供观察者或者其它工具来处理回调函数,在他们自己的实例变为不可达时,会让回调函数也变为不可达的。对于 setInterval,下面这样的代码是非常常见的:

  1.  
    var serverData = loadData();
  2.  
    setInterval(function() {
  3.  
    var renderer = document.getElementById(‘renderer‘);
  4.  
    if(renderer) {
  5.  
    renderer.innerhtml = JSON.stringify(serverData);
  6.  
    }
  7.  
    }, 5000); //This will be executed every ~5 seconds.

这个例子阐述着 timers 可能发生的情况:计时器会引用不再需要的节点或数据。

renderer 可能在将来会被移除,使得 interval 内的整个块都不再被需要。但是,interval handler 因为 interval 的存活,所以无法被回收(需要停止 interval,才能回收)。如果 interval handler 无法被回收,则它的依赖也不能被回收。这意味着 serverData——可能存储了大量数据,也不能被回收。在观察者模式下,重要的是在他们不再被需要的时候显式地去删除它们(或者让相关对象变为不可达)。

过去,特别是某些浏览器(IE6)无法管理循环引用。如今,大多数浏览器会在被观察的对象不可达时对 observer handlers 进行回收,即使 listener 没有被显式的移除。但是,明确地删除这些 observers 仍然是一个很好的做法。例如:

  1.  
    var element = document.getElementById(‘launch-button‘);
  2.  
    var counter = 0;
  3.  
    function onClick(event) {
  4.  
    counter++;
  5.  
    element.innerHtml = ‘text ‘ + counter;
  6.  
    }
  7.  
    element.addEventListener(‘click‘, onClick);
  8.  
    // Do stuff
  9.  
    element.removeEventListener(‘click‘, onClick);
  10.  
    element.parentNode.removeChild(element);
  11.  
    // Now when element goes out of scope,
  12.  
    // both element and onClick will be collected even in old browsers // that don‘t handle cycles well.

如今,现代浏览器(包括 IE 和 Edge)都使用的是现代垃圾回收算法,可以检测这些循环依赖并正确的处理它们。换句话说,让一个节点不可达,可以不必而在调用 removeEventListener。

框架和库,例如 jQuery ,在处理掉节点之前会删除 listeners (使用它们特定的 API)。这些由库的内部进了处理,确保泄漏不会发生。即使是在有问题的浏览器下运行,如。。。。IE6。

3:闭包

JavaScript 开发的一个关键方面就是闭包:一个可以访问外部(封闭)函数变量的内部函数。由于 JavaScript 运行时的实现细节,可以通过以下方式泄漏内存:

  1.  
    var theThing = null;
  2.  
    var replaceThing = function () {
  3.  
    var originalThing = theThing;
  4.  
    var unused = function () {
  5.  
    if (originalThing) // a reference to ‘originalThing‘
  6.  
    console.log("hi");
  7.  
    };
  8.  
    theThing = {
  9.  
    longStr: new Array(1000000).join(‘*‘),
  10.  
    someMethod: function () {
  11.  
    console.log("message");
  12.  
    }
  13.  
    };
  14.  
    };
  15.  
    setInterval(replaceThing, 1000);

这个代码片段做了一件事:每次调用 replaceThing 时,theThing 都会获得一个新对象,它包含一个大的数组和一个新的闭包(someMethod)。同时,变量 unused 保留了一个拥有 originalThing 引用的闭包(前一次调用 theThing 赋值给了 originalThing)。已经有点混乱了吗?重要的是,一旦一个作用域被创建为闭包,那么它的父作用域将被共享。

在这个例子中,创建闭包 someMethod 的作用域是于 unused 共享的。unused 拥有 originalThing 的引用。尽管 unused 从来都没有使用,但是 someMethod 能够通过 theThing 在 replaceThing 之外的作用域使用(例如全局范围)。并且由于 someMethod 和 unused 共享 闭包范围,unused 的引用将强制保持 originalThing 处于活动状态(两个闭包之间共享整个作用域)。这样防止了垃圾回收。
当这段代码重复执行时,可以观察到内存使用量的稳定增长。当 GC 运行时,也没有变小。实质上,引擎创建了一个闭包的链接列表(root 就是变量 theThing),并且这些闭包的作用域中每一个都有对大数组的间接引用,导致了相当大的内存泄漏,如下图:

技术分享图片

这个问题由 Meteor 团队发现的,他们有一篇伟大的文章,详细描述了这个问题。

4:DOM 引用

有时候,在数据结构中存储 DOM 结构是有用的。假设要快速更新表中的几行内容。将每行 DOM 的引用存储在字典或数组中可能是有意义的。当这种情况发生时,就会保留同一 DOM 元素的两份引用:一个在 DOM 树种,另一个在字典中。如果将来某个时候你决定要删除这些行,则需要让两个引用都不可达。

  1.  
    var elements = {
  2.  
    button: document.getElementById(‘button‘),
  3.  
    image: document.getElementById(‘image‘)
  4.  
    };
  5.  
    function doStuff() {
  6.  
    elements.image.src = ‘http://example.com/image_name.png‘;
  7.  
    }
  8.  
    function removeImage() {
  9.  
    // The image is a direct child of the body element.
  10.  
    document.body.removeChild(document.getElementById(‘image‘));
  11.  
    // At this point, we still have a reference to #button in the
  12.  
    //global elements object. In other words, the button element is
  13.  
    //still in memory and cannot be collected by the GC.
  14.  
    }

还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,可以假设 GC 将收集除了该单元格之外所有的内容。实际上,这不会发生的:该单元格是该 table 的子节点,并且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会导致整个表都保留在内存中的。保留 DOM 元素的引用时,需要仔细考虑。

 


以上是关于关于内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

FragmentStatePagerAdapter 内存泄漏(带有 viewpager 的嵌套片段)

带有 UI 和内存泄漏的保留片段

在片段中保存活动实例:是否会导致内存泄漏?

片段 - 全局视图变量与本地和内部类侦听器和内存泄漏

iPhone内存泄漏问题?

使用导致内存泄漏的音频片段