Skip to content

浏览器中的内存泄露 #7

@moyui

Description

@moyui

好久没有更新文章了,作为应届生自己也入职了第一家公司,同时在工作中也碰到了一些问题,不同于之前的原理性文章,相对来说这篇文章偏向业务侧与实战。

浏览器中的内存泄露

什么是内存泄露

在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费,导致成寻运行速度减慢甚至系统崩溃等严重后果。

泄露了什么

JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。

基本类型有六种: null,undefined,boolean,number,string,symbol。

基本类型的使用不会导致内存泄露,只有引用类型会导致内存泄露

内存泄露常见的类型

1.全局

全局分为全局变量与全局绑定事件

全局变量

function foo() {
  this.var1 = 'gloabl';
}
// Foo 被调用时, this 指向全局变量(window)
foo();

这里如果是单页应用的话切换页面不会销毁该变量

全局绑定事件

mounted() {
    window.addEventListener("scroll", this.func);
}
// 忘记在beforeDestoryed销毁了

同全局变量

2.定时器

setInterval(() => {
  let node = this.$refs.dom;
  console.log('timer goes on');
  if (node) {
    data.a++;
    node.textContent = JSON.stringify(data);
  }
}, 1000);

这里以 vue 为例子,如果当页面跳转的时候 dom 节点消失了,但是由于定时器并没有解除绑定,造成了内存泄露,并且由于做了容错处理并没有能够及时发现。

3.绑定的事件

const element = document.getElementById('launch-button');
const counter = 0;

function onClick(event) {
  counter++;
  element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);
// 发生内存泄露啦
element.parentNode.removeChild(element);

这里删除节点之前没有及时解绑事件,造成内存泄露。

如今,现在的浏览器(包括 IE 和 Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用 removeEventListener。框架和插件例如 jQuqery 在处理节点(当使用具体的 api 的时候)之前会移除监听器。

4. 闭包

let theThing = null;
let replaceThing = function() {
  let originalThing = theThing;
  let unused = function() {
    if (originalThing) console.log('hi');
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function() {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);

可以看到,每次在定时器中调用 replaceThing,由于 originalThing 所在的作用域中有函数 unused 引用了 originalThing 形成了闭包,并且每次会有一个大对象 theThing 中赋值了大数组 longstr 与方法。所以占用了很多内存。

但是个人觉得闭包本身并不是导致内存泄露的主要原因,闭包造成的内存占用是正常的并且是合理的。只有当闭包与相关代码(定时器)等结合之后才能造成内存泄露。

5. dom 引用

class ImageLazyLoader {
  constructor($photoList) {
    $(window).on('scroll', () => {
      this.showImage($photoList);
    });
  }
  showImage($photoList) {
    $photoList.each(img => {
      // 通过位置判断图片滑出来了就加载
      img.src = $(img).attr('data-src');
    });
  }
}

// 点击分页的时候就初始化一个图片懒惰加载的
$('.page').on('click', function() {
  new ImageLazyLoader($('img.photo'));
});

// 解决方法
// 新增一个事件解绑
clear () {
  $(window).off('scroll', this.scrollShow);
}

scroll 绑定形成了一个闭包,this、$photoList这两个变量一直没有被释放,与上一例子不同的是$photoList 是一个 dom 节点,当清除掉上一页的数据的时候,相关 DOM 结点已经从 DOM 树分离出来了,但是仍然还有一个$photoList 指向它们,导致这些 DOM 结点无法被垃圾回收一直在内存里面,就发生了内存泄露。

解决方法

内存泄露的一般以 bug 作为处理,相对而言处理方法比较简单

  1. 针对定时器

在生命周期结束之后关闭定时器

clearTimeOut(this.timer);
  1. 针对全局变量闭包
及时将对应变量设置为null;
  1. 针对事件
及时解绑事件(部分框架做好了)
removeEventListener
  1. 针对 dom
对应变量设置为null
  1. weakset 与 weakmap

这里着重探讨一下这两个数据结构,其实在平时开发过程中这两个都相对较少会用到。

特点:

  1. WeakSet 对象中只能存放对象引用, 不能存放值, 而 Set 对象都可以, weakmap 的 key 同样也只能存放对象应用,而 map 都可以
  2. WeakSet 对象中存储的对象值都是被弱引用的, 如果没有其他的变量或属性引用这个对象值, 在垃圾回收之后会被销毁,weakmap 的 key 也是同理。
  3. weakmap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
  4. 注意,以上两个数据结构是不可枚举的,weakset 甚至不能拿到元素,只能判断元素在不在 weakset 里

weakset 的用途:

  1. 无需关联上下文时保存变量
const requests = new WeakSet();
class ApiRequest {
  constructor() {
    requests.add(this);
  }
  makeRequest() {
    if (!request.has(this)) throw new Error('Invalid access');
    // do work
  }
}

这里可以看到我们只需要判断 this 而不需要使用 this 的引用,这里使用 weakset 就相对好很多。

weakmap

  1. 以 DOM 节点作为键名的场景
const wm = new WeakMap();
const ele = document.getElementById('example');
wm.set(el, 'some information');
wm.get(el);
  1. 注册定时器,在 dom 节点被删除时销毁
const listener = new WeakMap();

listener.set(ele1, handler1);
listener.set(ele2, handler2);

ele1.addEventListener('click', listener.get(ele1), false);
ele2.addEventListener('click', listener.get(ele2), false);

如何判断内存泄露

在实际项目开发中,内存泄露通常是很难被找到的,只有项目中明显出现了异常报错可以及时能够解决,通常情况下除了对性能要求比较高的场景,很少会去测量页面中存在的内存泄露,第一是工作量比较大,第二是由于项目中是多页应用,跳转到新页面时前一个页面会被销毁,只保留快照,内存中的垃圾也随之回收,所以问题不明显,只有当用户在单一页面中停留较长时间才可能感知到页面卡顿等情况出现。

综上,从工程角度来看,内存泄露分为周期性的内存增长导致泄露与偶现的,周期性的内存增长,这种是开发阶段中容易被感知到的,也是容易被解决的。偶现的内存泄露一般可以忽视。

使用前先点击内存回收按钮,并且使用浏览器的隐私模式

1.timeline 工具的使用

1.png

在 performance 面板中勾选上 memory 并且开始录制。

2.png

对页面进行相应操作,录制结束之后可以看到内存堆的变化(js heap)可以看到垃圾回收不断执行,可以判断发生了严重的内存泄露。

2.memory 工具的使用

  1. hotspots

3.png

点击 memory,常用的是选择第一个和第二个选项。

4.png

点击第一个选项,设定基准(snapshot 1),在页面进行操作之后保存基准 2(snapshot 2),进行对比,我们主要关注 size delta 中为正值的情况。

5.png

可以看到中间有大量的字符串存放在变量中,在下方也能看到是变量名为 x 的变量。

  1. allocation instrumentation on timeline

6.png

选择第二项查看。

7.png

点击开始后会自动进行分析。

8.png

我们更加关注每段灰色与蓝色之间相差最小的部分,可以看到同样找到了存放大变量的位置。

总结

以上就是这次全部的内容,相对来说更偏向于实战与业务,最近这段时间对计算机图形学比较感兴趣,同时公司技术栈也要切换到 React/RN/Taro 方向了,应该会多出这一类的文章。

参考文章

  1. [译] JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄漏(译)

  2. What are the actual uses of ES6 WeakMap?

  3. 4类 JavaScript 内存泄漏及如何避免

  4. https://juejin.im/post/5b2fd09ee51d45588576f429

  5. WeakSet 用法解惑

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions