前端常见内存泄漏及识别方法

1. 什么是内存泄漏

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

在 C 语言中,必须手动释放内存,程序员负责内存管理。

在 C 语言中,malloc 方法用来申请内存,使用完毕之后,必须用 free 方法释放内存

显然这样很麻烦,所以大多数语言都提供内存管理,被称为垃圾回收机制(garbage collector)。

2. 常见的内存泄漏

1) 全局变量

function foo(arg) {
    bar = "some text";
}

在 JS 中处理未被声明的变量, 上述范例中的 bar 时, 会把 bar , 定义到全局对象中, 在浏览器中就是 window 上. 在页面中的全局变量, 只有当页面被关闭后才会被销毁. 所以这种写法就会造成内存泄露, 当然在这个例子中泄露的只是一个简单的字符串, 但是在实际的代码中, 往往情况会更加糟糕.

另外一种意外创建全局变量的情况.

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

在这种情况下调用 foo, this被指向了全局变量 window, 意外的创建了全局变量.

我们谈到了一些意外情况下定义的全局变量, 代码中也有一些我们明确定义的全局变量. 如果使用这些全局变量用来暂存大量的数据, 记得在使用后, 对其重新赋值为 null.

2) 未销毁的定时器和回调函数

在很多库中, 如果使用了观察着模式, 都会提供回调方法, 来调用一些回调函数. 要记得回收这些回调函数. 举一个 setInterval的例子.

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除, 整个定时器实际上没有任何作用. 但如果你没有回收定时器, 整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收. 在这个案例中的 serverData 也无法被回收.

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`Hello, my name is ${this.name}!`);
    }
}

function sayHello() {
    const tom = new Person("Tom");
    const jerry = new Person("Jerry");
    tom.sayHello(); // tom 对象在执行完毕之后就被回收了
    let count = 0;
    setInterval(() => {
        if (count < 3) {
            jerry.sayHello(); // 如果定时器没有被销毁,jerry 对象就无法被回收
            count++;
        }
    }, 100);
	/**
	// 正确做法是在定时器执行结束之后销毁掉
	const intervalId = setInterval(() => {
		if (count < 3) {
            jerry.sayHello();
            count++;
        } else {
			clearInterval(intervalId);
		}
	})
	*/
}

sayHello();

3) 闭包

在 JS 开发中, 我们会经常用到闭包, 一个内部函数, 有权访问包含其的外部函数中的变量. 下面这种情况下, 闭包也会造成内存泄露.

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 对于 'originalThing'的引用
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

这段代码, 每次调用 replaceThing 时, theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象. 同时 unused 是一个引用了 originalThing 的闭包.

这个范例的关键在于, 闭包之间是共享作用域的, 尽管 unused 可能一直没有被调用, 但是someMethod 可能会被调用, 就会导致内存无法对其进行回收. 当这段代码被反复执行时, 内存会持续增长.

该问题的更多描述可见Meteor团队的这篇文章.

4) 脱离 DOM 元素的引用

很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中.

var elements = {
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    document.body.removeChild(document.getElementById('image'));
    // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收. 
}

上述案例中, 即使我们对于 image 元素进行了移除, 但是仍然有对 image 元素的引用, 依然无法对齐进行内存回收.

另外需要注意的一个点是, 对于一个 Dom 树的叶子节点的引用. 举个例子: 如果我们引用了一个表格中的 td 元素, 一旦在 Dom 中删除了整个表格, 我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素. 但是事实上, 这个 td 元素是整个表格的一个子元素, 并保留对于其父元素的引用. 这就会导致对于整个表格, 都无法进行内存回收. 所以我们要小心处理对于 Dom 元素的引用.

5) Map 对象

Map 对象中的 key 无法被垃圾回收机制回收,改为 WeakMap 。

6) 注册到全局的事件

3. 内存泄漏的识别方法

Chrome 浏览器查看内存占用,按以下步骤操作:

  1. 打开开发者工具,选择 Performance 面板
  2. 顶部默认勾选了 Screenshots ,把它去掉,勾选 Memory
  3. 点击 Start profiling and reload page
  4. 然后页面就会开始刷新,同时进行录制
  5. 等待页面加载完成后,点击 stop 按钮,面板上就会显示内存占用情况

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。反之,就是内存泄漏了。

在这里插入图片描述

参考:
JavaScript 内存泄漏教程
精读《JS 中的内存管理》

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值