Skip to content

【JS】内存泄露及如何避免? #25

@Dliling

Description

@Dliling

内存泄露
指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。简单来说,就是程序不再需要占用内存的时候,由于某些原因,内存没有被释放仍被不必要的占用着。

JS中,有垃圾回收机制。通过周期性地检查之前分配的内存是否还在使用中,即变量不再需要后将其自动清除。但是,当代码有缺陷时,你以为你已经不需要,但是程序中还存在引用,就不能被回收,导致内存不断的占用,运行时间越长内存占用的越多,随之就会出现性能不佳,运行减慢甚至系统崩溃。

垃圾回收算法

  1. 标记清除
    当变量进入环境时,就将这个变量标记为"进入环境"。逻辑上来讲,永远不能释放进入环境的变量所占用的内存。因为,进入环境以为着变量可能被用到。当变量离开环境时,则将其标记为"离开环境"。
    垃圾收集器在运行的啥时候会给存储在内存中的所有变量都加上标记,然后,他回去掉环境中的变量以及被环境中的变量引用的变量的标记。再次之后,有标记的变量即被认为是准备删除的变量。最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收他们占用的内存。

缺点:存在循环引用时,存在缺陷。如下面所示:

var obj1 = new Object();
var obj2 = new Object();
obj1.name = obj2;
obj2.job = obj1;
  1. 引用计数
    跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值赋值给该变量时,则这个值得引用次数就是1。如果同一个值又被赋值给另一个变量,该值的引用次数加1 。相反,若包含这个值的引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明这个值没有被引用了,因而可以将它所占用的内存空间回收。

现代的垃圾收集器改良了算法,但是本质是相同的:被引用的值所占用的内存保留,其余的被当做垃圾回收。

易于理解的垃圾回收可以移步这里

不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍没有被释放。

为了理解JS中常见的内存泄露,我们需要了解引用通常被遗忘的方式。
常见的内存泄露

1. 全局变量
在非严格模式下当引用未声明的变量时,会在全局变量中创建一个新变量。在浏览器中,全局变量将是window。

function foo(arg) {
    bar = 'abc';  // 将泄露到全局
}

原因:全局变量根据定义是无法被垃圾收集器回收的。如果必须用全局变量来存储数据,请确保将其指定为null或在完成后将其解除引用。

function foo(arg) {
    bar = 'abc';  // 将泄露到全局
    bar = null;
}

解决方法:使用严格模式

2.被遗忘的定时器或回调函数
在JS中经常使用setInterval或者setTimeout

setInterval(function () {
    var node = document.getElementById('app');
    if (node) {
        console.log(123);
    }
}, 1000);

原因:与节点相关的计时器不再需要时,node对象可以删除,整个回调函数也不需要了。可是,计时器并没有被回收。只有停止时才会停止。
解决方法:在不需要定时器时,手动清除。

通过addEventListener监听事件时,传入回调函数。老版本的IE时无法检测DOM节点和JS代码之间的循环引用,会导致内存泄露。如今,现代的浏览器更新了垃圾回收机制,已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用removeEventListener了。

3. 脱离DOM的引用

var refA = document.getElementById('box');
document.body.removeChild(refA);
console.log(refA);

原因:保留了DOM节点的引用,导致没有被回收
解决方法:refA = null;

还要考虑DOM树内部或子节点的引用问题。

4. 闭包
闭包使用正确时,不会引起内存泄露!!!

var replaceThing = function() {
    var originalThing = theThing;
    var unused = function() {
         if (originalThing) {
            console.log('hi');
        }
    }
    theThing = {
        someMethod: function() {
            console.log('abc');
        }
    };
};
setInteral(replaceThing, 1000);

someMethod通过theThing使用,someMethodunused共享闭包作用域,尽管unused从未被使用,它引用的originalThing迫使它保留在内存中。当这段代码反复执行时,内存不断上升。
解决方法:在replaceThing最后添加originalThing = null

分析内存泄露时,我们可以通过chrome开发者工具提供的performance。记录一段网页操作后进行分析。如:
infoflow 2020-06-18 21-35-33

具体方法可见这里

参考
https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/

今天看到前端之巅分享的一篇关于内存泄露的文章,感觉不错,贴出来~JavaScript 中内存泄漏的原因以及对策

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