字节面试题--请优化一下这一段内存泄漏代码

571 阅读5分钟

一:引言

前几天去面试了一下字节的影像部门,被问到了一个关于浏览器内存泄漏非常有趣的题目。 通过这一题可以让我们从定量和定性的角度去思考浏览器内存管理这一件事情。

二:题目解析

面试题目:请讲下如下代码可能会发生内存泄露的地方和优化的方案。

 <button onclick="replaceThing()">第二次点我就有泄漏</button>
  <script>
  var theThing = null;
  var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
          if (originalThing) {
              console.log("hi");
          };
      }
      theThing = {
          longStr: new Array(1000000),
          someMethod: function someMethod() {
              console.log('someMessage');
          }
      };
  };
  </script>

2.1 内存泄漏思考

首先当我们去思考什么时候会发生内存泄漏。

  1. 使用了全局变量
  2. 使用了闭包
  3. 对dom的引用在dom树卸载后没有置空
  4. 遗忘的定时器 在这一块代码中我们从可能情况中进行过滤,察觉到可能是由于闭包导致的内存泄漏。 从unused这一个函数来看,里面调用了originalThing这一块内存变量,本来会导致闭包的出现使originalThing这一个变量无法被释放,但是由于unuesed函数没有进行使用过,所以浏览器会帮我们做这一块内存闭包的释放。 但一个问题是这样整个的结果就不会导致内存泄漏了嘛?

2.2 从定性角度探究内存泄漏

首先我们需要先探究下这一块代码是否会发生泄漏: 从浏览器给我们带来的内存检测手段来看。

  1. 使用性能监控器中的js堆大小来查看

image.png 2. 使用性能录制工具来查看内存变化情况

image.png 从这里我们就可以看到内存是在泄漏且不断地有一个增大的过程。

2.3 从定量角度探究内存泄漏

那接下来我们就要从定量去分析一下是什么情况会导致这一种内存泄露的情况出现。 我们打开开发者工具的内存项来具体分析该页面使用到的内存空间:

image.png 点击一下获取快照按钮来截取快照信息

image.png 接着我们再触发一下有可能会产生内存泄露的方法(在本文中就是下面的这一个按钮)。然后再进行一次手动GC,最后点击获取快照按钮来获取到当前堆中的数据情况。 image.png 第二次快照情况如下:

image.png 这里就有两个快照信息了,分别为可能发生内存泄露的方法触发前和触发后的内存情况。

image.png 这里我们通过比较来查看到底是什么数据无法被GC回收。

image.png 我们从内存的比较可以看到会有如下的这一种重重引用关系。

image.png 到这一步我们就了解为啥会导致内存泄漏这一个问题了,就是因为theThing这一个变量始终在闭包空间中没办法得到释放。 那一个问题为啥theThing这一个变量会出现在闭包空间中讷。theThing又没有在someMethod中进行调用。 解决方案: 消除掉对前一个theThing的引用就好了。

 <button onclick="replaceThing()">第二次点我就有泄漏</button>
  <script>
  var theThing = null;
  var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
          if (originalThing) {
              console.log("hi");
          };
      }
      theThing = {
          longStr: new Array(1000000),
          someMethod: function someMethod() {
              console.log('someMessage');
          }
      };
      // 添加这一行消除掉对前一个theThing的引用。
      originalThing = null;
  };
  </script>

那接下来我们看下闭包它的策略性回收问题吧。

五:看懂闭包内存管理机制

闭包简介

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数被全局环境下的变量引用,就形成了闭包。

闭包实质上是函数作用域的副产物。

关于闭包我们需要特别重视的一点是函数内部定义的所有函数共享同一个闭包对象
什么意思呢?看如下代码:

var a
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()

上面代码中f引用了变量d,同时f被外部变量a引用,所以形成闭包,导致变量d滞留在内存中。我们思考一下,那么变量c呢?好像我们并没有用到c,应该不会滞留在内存中吧。然后事实是c也会滞留在内存中。如上代码形成的闭包包含两个成员,c和d。这种现象成为函数内闭包共享

从内存结构图中我们可以用一个更直观的方式来查看数据之间的引用关系。 image.png

灰色表示的图形是内存垃圾,将会被垃圾回收器回收。

上图很容易得出,虽然函数f没有用到变量c,但是c被函数e引用,所以变量c存在于闭包closure中,从window对象开始寻找能够找到变量c,所以变量c也不能释放。

六:内存对性能的影响程度?

从一个更直观的浏览器渲染响应图来看

image.png 当内存泄漏过多的时候就会导致不断产生GC任务来清理无用的内存空间给新数据腾出空间来。同时GC任务时间较长会占用js引擎的时间来使得其他交互任务会被堵塞住。所以最好是在日常开发过程中对内存泄漏这一件事情有一个更深的认识,避免写出这一种会导致大量内存泄漏而最后影响用户体验的代码。

七:总结

没想到这一个闭包的问题会引出来对闭包内存数据的管理,如何使用开发者工具来对内存泄漏情况进行辨别和确认的这一个过程,每一件小问题背后都隐藏着各种各样的深层含义我们必须要能够更加灵活认真的去对待遇到的每一件事情,这样不断地思考总结归纳分析,让平凡地任务不简单。

参考文章: 浏览器是怎么看闭包的。 - 掘金 (juejin.cn) Grokking V8 closures for fun (and profit?) (mrale.ph)