【前端--JavaScript】知识点(六)—— 项目中的内存泄漏(二)

362 阅读5分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

上一篇中说到,不经意的内存泄露会导致程序变卡,甚至会出现页面崩溃,下面通过例子来从浏览器的角度排查是否存在内存泄露,怎么定位内存泄露。

内存泄露排查

例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>test</title>
</head>
<body>
  <button id="click">click</button>
  <h1 id="content"></h1>
  <script>
    let click = document.querySelector("#click");
    let content = document.querySelector("#content")
    let arr = [];
    function testFun() {
      let test = new Array(1000).fill('1111')
      return function () {
        return test
      }
    }
    click.addEventListener("click"function () {
      arr.push(testFun())
      arr.push(testFun())
      content.innerHTML = arr.length
    });
  </script>
</body>
</html>

这是一个不正当使用闭包构成的内存泄露的例子。 代码中,为页面的button绑定了一个点击事件,每次点击都执行两次闭包函数,并把结果push 到全局的数组arr中,由于闭包函数执行结果也是一个函数,并且存在对原闭包函数内部数组test的引用,所以arr数组中的每一项元素都使得引用的闭包内部test数组对象没有办法回收,arr数组有多少元素,也就存在多少次闭包引用,我们点击的越多,push就越多,内存就消耗越大,页面也会越来越卡。

这是我们知道问题的情况下,对内存泄露的排查,当项目庞大的时候我们就没法直接定位到具体代码了,这时候就需要借助浏览器控制台(Chrome Devtool)了。

首先打开浏览器的无痕模式,打开程序控制台:

image.png 然后找到Performance,他是用来监控性能指标的工具,可以记录分析在网站的生命周期内发的各类事件:

image.png

接下来开始操作,在开始之前一定要确认勾选了 Memory (序号5)选项 ,这样我们才可以看到内存相关的分析。

点击开始录制(序号 1)进入录制状态,随后先清理一下GC(垃圾回收),也就是最右边的垃圾桶(序号6)。

然后一直点击按钮50次,页面数值应该是100,再点击一下垃圾桶,手动触发一次GC(垃圾回收),再点击150次,然后停止录制。

image.png 我们可以明显看到内存数据是一个不断上涨的趋势,而且即使我们在50次的时候手动垃圾回收了一次,清理后的内存也没有少很多。

image.png

内存泄露分析定位

在这段代码中,我们可以很快的知道是点击按钮的操作出现了内存泄露,但真实项目中,项目过于庞大,操作过多的情况下,是不容易很快定位到具体问题的。这时候需要借助Memory,它可以记录JS中CPU执行时间细节,显示JS对象和相关的DOM节点的内存消耗内存的分配细节等。

Memory中的Heap Profiling 可以记录当前的堆内存 heap 的快照,并生成对象的描述文件,该描述文件给出了当下 JS 运行所用的所有对象,以及这些对象所占用的内存大小、引用的层级关系等等,用它就可以定位出引起问题的具体原因以及位置。

image.png

我们点击左边的生成快照,就生成了Snapshot 1,它代表了刚刚这一刻的内存状态。

image.png

快照这边有一个下拉框:

  • Summary:按照构造函数进行分组,捕获对象和其使用内存的情况,可理解为一个内存摘要,用于跟踪定位DOM节点的内存泄漏
  • Containment:探测堆的具体内容,提供一个视图来查看对象结构,有助分析对象引用情况,可分析闭包及更深层次的对象分析
  • Statistics:统计视图

默认显示的是Summary,下面的信息就代表快照生成的一瞬间,内存中存了什么,占用内存的信息等。

image.png Summary中数据表格的信息表示什么:

  • Constructor:显示所有的构造函数,点击每一个构造函数可以查看由该构造函数创建的所有对象
  • Distance:显示通过最短的节点路径到根节点的距离,引用层级
  • Shallow Size:显示对象所占内存,不包含内部引用的其他对象所占的内存
  • Retained Size:显示对象所占的总内存,包含内部引用的其他对象所占的内存

image.png 我们这时候选择第三个直接进行对比:

  • system、system/Context 表示引擎自己创建的以及上下文创建的一些引用,这些不用太关注,不重要
  • closure 表示一些函数闭包中的对象引用

image.png

在这里我们点开可以看到,代码在19行存在内存泄露,同时看Array,也代表着全局变量要是在页面关闭前没清理,就会一直存在内存中。

这样我们就成功了定位到了内存泄露的原因。 在实际项目中,问题可能会更加复杂,我们要根据具体场景选择解决方案,解决之后重复上面排查流程就行排查。

内存三大件

在这里顺便了解一下什么是内存三大件,也就是内存方面主要的三个问题:

内存泄漏 我们说很久了,对象已经不再使用但没有被回收,内存没有被释放,即内存泄漏,那想要避免就避免让无用数据还存在引用关系,也就是多注意我们上面说的常见的几种内存泄漏的情况。

内存膨胀 即在短时间内内存占用极速上升到达一个峰值,想要避免需要使用技术手段减少对内存的占用。

频繁 GC 就是 GC(垃圾回收) 执行的特别频繁,一般出现在频繁使用大的临时变量导致新生代空间被装满的速度极快,而每次新生代装满时就会触发 GC,频繁 GC 同样会导致页面卡顿,想要避免的话就不要搞太多的临时变量,因为临时变量不用了就会被回收,这和我们内存泄漏中说避免使用全局变量冲突,其实,只要把握好其中的度,不太过分就 没事。