简单聊聊内存泄漏

271 阅读5分钟

前言

上文我们聊了下V8的内存管理,内容偏原理解析性质,与平时写代码的关联性没有太明显的关系,那么本文就来聊聊与内存相关的关联性大的内容 ------ 内存泄漏。

常见的内存泄漏

所谓的内存泄漏,其实就如字面意思,就是当不再用到的对象内存没有及时被回收时,就是内存泄漏。

那么我们常见的几种内存泄漏都有哪些呢?

在聊这个之前,先说明下,这里使用来确定是否有内存泄漏的方式是通过浏览器的devtool中的内存模块,如下图。

image.png

通过这个模块,先进行手动的GC(手动垃圾回收)再执行快照,如果在内存中还能找到相关数据,那就证明产生了内存泄漏。

除此之外,也能直接通过调试中的closure确定是否有闭包。

不合理的闭包

闭包:一个函数和对其词法环境的引用捆包在一起的组合。也就是说一个内层函数中访问到外层函数的作用于,在js中,每当创建一个函数,闭包就会这函数被创建的同时而创建出来。

例子如下:

function Person() {};
function fn() {
  const name = 'Rippi';
  const age = 10;
  const arr = new Array();
  for (let i = 0; i < 1000; i++) {
    arr.push(new Person());
  }
  return function() {
    console.log(arr, name);
  }
}
const persons = fn();
persons();

image.png

fn返回的匿名函数使用了name和arr,我们在图中可以看到name和arr都在closure中,而age被使用,所以age不存在于closure中。

隐式全局变量

全局变量通常不会被回收,所以要避免额外的全局变量,使用完毕之后重置为null即可。

例子如下:

function Person() {}
function fn() {
  p1 = new Person();
  this.p2 = new Person();
}
fn();

image.png

p1和p2均会被绑定在全局变量上,如果不手动将他们置为null,那么就会产生内存泄漏。

分离的DOM

当在界面中移除DOM节点时,还要移除相应的节点引用。

<body>
  <div id="container">
    <p id="title">title</p>
  </div>
  <script>
    const container = document.getElementById('container');
    const title = document.getElementById('title');
    document.body.removeChild(container);
  </script>
</body>

image.png

container节点已经在dom树上移除了,但还保持了引用,会造成内存泄漏,这种情况将container和title都置为null即可。

定时器

  • setInterval
  • setImmediate
  • requestAnimationFrame

其实定时器算是因为闭包产生的,不过这里还是单独列出来了,因为定时器产生的内存泄漏是最为常见的。

例子如下:

function Person(name) { this.name = name };
function fn() {
  const person = new Person('Rippi');
  const timer = setInterval(() => {
    person.age = 18;
  }, 2000);
}
fn();

image.png

Map、Set对象

Map和Set存储对象时,如果不主动清除,也会造成内存泄漏的问题。

例子如下:

function Person() {};
let obj = new Person();
const set = new Set([obj]);
const map = new Map([[obj, 'Rippi']]);
obj = null;

image.png

如图,我们可以看到,尽管将obj置为了null,但由于new出来的person仍然被set和map引用着,如果不将set和map也置为null,这个new出来的Person是无法被回收的。

在一些场景下,我们可以使用WeakMap和WeakSet代替Map和Set,因为他们并不会增加引用计数,当所存的对象被置为空的时候,那么就会被回收掉。

我们看个例子:

function Person() {};
let obj = new Person();
const set = new WeakSet([obj]);
const map = new WeakMap([[obj, 'Rippi']]);
obj = null;

image.png

通过这两个小例子,可以看出很大的区别,WeakMap和WeakSet并没有产生内存泄漏。

以上就是我们常见的会产生内存泄漏的情况了。

排查内存泄漏

我们既然了解内存泄漏是什么,以及怎样会产生内存泄漏,那么我们还需要知道如何排查内存泄漏。

其实本文开头已经说了两种排查内存泄漏的方法了,一个是利用好devtool的内存模块,另外就是利用好调试模块。

这里再补充一个,那就是利用好performence模块。

我们通过一个简单的例子来看看都如何操作吧。

<body>
  <div id="container">0</div>
  <button id="click">click</button>
  <script>
    function Person() {};
    const rows = [];
    function getColumns() {
      var col = new Array(100000).fill('0');
      for (let i = 0; i < col.length; i++) {
        col[i] = new Person();
      }
      return col;
    }
    click.addEventListener('click', () => {
      rows.push(getColumns());
      container.innerHTML = rows.length;
    });
  </script>
</body>
  • 通过performence查看内存变化

image.png

js堆内存曲线一直向上走说明很可能产生了内存泄漏了,那么我们接下来就可以通过内存模块排查具体问题了。

  • 通过内存快照

image.png

每一次快照内存都增加,而且就算手动GC(点击那个垃圾桶)也无法减少内存,说明肯定内存泄漏了。

  • 对比快照找出问题

对比快照的功能在过滤器旁边的下拉框中,选择完成后,左侧可以选择对比的目标快照。

image.png

image.png

这里我们拿第四个快照和第三个快照进行对比。

image.png

这样就能定位到产生快照的大概位置了。

结尾

以上就是本文所有内容了,本文主要通过多个例子一一介绍了产生内存泄漏的场景,最后通过一个简单例子讲解了如何排查与定位内存泄漏。

最后,希望各位都能有所收获。

最后的最后,也希望看到此处的大家能给个小赞🌹

上一篇文章:简单聊聊V8内存管理 - 掘金 (juejin.cn)