彻底掌握js内存泄漏以及如何避免

14,912 阅读8分钟

前言:内存泄漏写任何语言都必须得注意的问题,我司技术老大日常吐槽:以前做游戏的改内存泄漏的bug,现在写前端还是这些问题。

什么是内存泄漏

内存泄漏可以定义为程序不再使用或不需要的一块内存,但是由于某种原因没有被释放仍然被不必要的占有。在代码中创建对象和变量会占用内存,但是javaScript是有自己的内存回收机制,可以确定那些变量不再需要,并将其清除。但是当你的代码存在逻辑缺陷的时候,你以为你已经不需要,但是程序中还存在着引用,导致程序运行完后并没有合适的回收所占用的空间,导致内存不断的占用,运行的时间越长占用的就越多,随之出现的是,性能不佳,高延迟,频繁崩溃。 在深入了解内存泄漏之前,我们需要知道一下几点:

  1. 内存生命周期
  2. 内存管理系统
  3. 垃圾回收算法

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:文章封面图

  • 分配你所需要的内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放\归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。

内存管理系统:手动or自动

不同的语言通过不同的方式来处理其内存。

  • 低级语言:像C语言这样的低级语言一般都有底层的内存管理接口,比如 malloc()和free()。
  • 高级语言:JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

垃圾回收算法

垃圾回收机制通过定期的检查哪些先前分配的内存“仍然被需要”(×) 垃圾回收机制通过定期的检查哪些先前分配的内存“程序的其他部分仍然可以访问到的内存”(√)。

嗯? 一脸懵逼。。。。

这是理解垃圾回收的关键,只有开发者知道这一块内存是否在未来使用“被需要”,但是可以通过算法来确定无法访问的内存并将其标记返回操作系统。

  • 引用计数
  • 标记清除

1. 引用计数

这是最初级的垃圾收集算法,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。(MDN上面的例子)

var o = { 
    a: {
        b:2
    }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,
另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集

var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”的原始引用o被o2替换了

var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 最初的对象现在已经是零引用了
       // 他可以被垃圾回收了
       // 然而它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
       // 它可以被垃圾回收了

缺陷:在循环的情况下,引用计数算法存在很大的局限性。

  function  foo(){
       var  obj1  = {};
       var  obj2  = {};
       obj1.x  =  obj2 ; // obj1引用obj2
       obj2.x  =  obj1 ; // obj2引用obj1
       return true ;
   }
   foo();

2. 标记清除

算法由以下几步组成:

  1. 垃圾回收器创建了一个“roots”列表。Roots通常是代码中全局变量的引用。JavaScript中,“window”对象是一个全局变量,被当作root。window对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
  2. 所有的 roots被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从root开始的所有对象如果是可达的,它就不被当作垃圾。
  3. 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。

无图言( )

循环引用的问题迎刃而解.虽然这个算法在不停的改进,js垃圾回收的(生成/增量/并发垃圾回收)这些改进的本质上还是相同的: 可达内存被标记,其余的被当作垃圾回收。

缺点: 算法运行时程序执行被暂停。

垃圾回收机制不可预测

虽然垃圾回收机制很好很方便,但是得自己权衡.原因是我们无法确定何时会执行收集.只有开发人员才能明确是否可以将一块内存返回给操作系统。不需要的引用是指开发者明知内存引用不再需要,而我们在写程序的时候却由于某些原因,它仍被留在激活的 root 树中。

如何避免

垃圾收集语言泄漏的主要原因是不需要的引用。要了解不需要的引用是什么,首先我们需要了解垃圾收集器如何确定是否可以访问一块内存。

因此,要了解哪些是JavaScript中最常见的泄漏,我们需要知道引用通常被遗忘的方式

常见的四种内存泄漏

1. 全局变量

在非严格模式下当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg){ 
    bar =“some text”; // bar将泄漏到全局.
}

为什么不能泄漏到全局呢,我们平时都会定义全局变量呢!!!

** 原因 ** :全局变量是根据定义无法被垃圾回收机制收集.需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,请确保将其指定为null或在完成后重新分配它。 ** 解决办法 **: 严格模式

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

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
        // 定时器也没有清除
    }
    // node、someResource 存储了大量数据 无法回收
}, 1000);

原因:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。

解决方法: 在定时器完成工作的时候,手动清除定时器

3. DOM引用

var refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, "refA");  // 但是还存在引用
能console出整个div 没有被回收

原因: 保留了DOM节点的引用,导致GC没有回收

解决办法:refA = null;

注意: 此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。

4. 闭包

注意

注意

注意: 闭包本身没有错,不会引起内存泄漏.而是使用错误导致.

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

这是一段糟糕的代码,每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了theThing)。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。

解决: 去除unuserd函数或者在replaceThing函数最后一行加上 originlThing = null.

参考:

4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them

How JavaScript works: memory management + how to handle 4 common memory leaks