likes
comments
collection
share

聊下内存泄露

作者站长头像
站长
· 阅读数 9

前言

不知道各位前端大佬,平时有没有遇到过内存泄露,然后整个页面都卡死的情况呢?

我在最开始接手公司项目的时候就曾经遇到这种问题,页面切换多几遍就会很卡顿,后面排查才知道是因为定时器使用有问题,频繁创建最后导致内存泄露了。

这个也是面试的常见问题,所以我们这里来聊下内存泄露吧,什么情况下会导致内存泄露。

内存泄露是什么

内存泄露指在程序执行的过程中,系统分配的内存没有被正常释放,导致内存一直被占用浪费,轻则程序性能被影响,重则进程崩溃

需要注意内存泄露和内存溢出是不一样的,内存泄露是有部分内存已经无法控制了,而内存溢出指分配的内存不够

那为什么系统分配的内存会无法控制呢?

众所周知,我们在执行程序的时候会经历三个步骤:分配内存 > 使用内存 > 回收内存

我们平时看到突然内存激增,页面卡顿是否意味着内存泄露呢?不一定,有可能是代码写的不好,比如突然进行了大量占用内存的操作,但是如果后续内存可以正常回收,那就不算内存泄露。

对于js而言,它有一套自己的垃圾回收机制,内存泄露指即便程序不再需要这部分内存,但是垃圾回收机制依旧认为还在使用,所以无法回收这部分内存导致内存泄露

内存泄露本质上指:程序失去对申请的内存失去控制,无法释放。

常见的内存泄露场景

意外的全局变量

JavaScript常运行在浏览器环境中,this指向的是window,this上添加变量是属于全局对象window的,全局变量不会被回收,除非重新指向null。

因此如果我们意外定义了不需要的全局变量,就会导致出现部分无法回收的内存。 我们不妨验证下:

测试文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button onclick="memory()">内存泄露</button>
    <script type="text/javascript">
        function memory() {
         for (let index = 0; index < 10000; index++) {
            this[index + 'key']=index
         }
        }
    </script>
        
</body>
</html>

性能监控效果

聊下内存泄露

可以清楚看到我们执行myFunction函数之后。进行了一万次往window上挂载变量的操作,当我们手动执行GC的时候会发现最后有一段内存下不来,意味着这部分内存已经泄露了。

console.log()

作为程序员我们看到console.log(),用来打印数据,定位错误啥的还是很方便的。 频繁使用console.log也会导致内存泄露,因为打印的对象需要被展示,因此log的时候会打印出去这个对象快照,当我们点开log内容时就会去内存读取实际的内容,因此一般情况下我们会清理生产环境的console.log 我们不妨验证下:

测试文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button onclick="memory()">内存泄露</button>
    <script type="text/javascript">
        function memory() {
         for (let index = 0; index < 10000; index++) {
            console.log({a: 1});
         }
        }
    </script>
        
</body>
</html>

性能监控效果

聊下内存泄露

可以看到我们执行myFunction函数之后。进行了一万次log操作,当我们手动执行GC的时候会发现最后有一段内存下不来,意味着这部分内存已经泄露了。

闭包

闭包也是我们日常开发的常用技巧,举个例子函数A中定义函数B,函数B调用了在函数A内定义的变量,并且在函数A外部被调用,这就形成了闭包

为什么说闭包会导致内存泄露呢?

不妨写段代码演示下:

function test() {
    let a = {}
    for (let index = 0; index < 10000; index++) {
        a[index + 'key']=index   
    }
    let inner = function () {
        return a
    }
    return inner
}
let obj
function Closure() {
    obj = test()
}

上面的代码通过Closure函数触发开始。

我们通过js执行栈去解释这段代码,首先执行Closure函数与之相关的上下文入栈,随后执行test函数与之相关的上下文入栈,在test函数中返回了inner函数,由于inner函数中使用的变量a指向test函数上下文,所以test函数无法出栈,同样的因为Closure函数调用全局的obj变量,因此Closure函数也无法出栈,只要obj函数不执行,test函数无法出栈,那么这部分内存就会一直无法释放,导致内存泄露。

测试文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button onclick="Closure()">闭包</button>
    <script type="text/javascript">

        function test() {
            let a = {}
            for (let index = 0; index < 10000; index++) {
                a[index + 'key']=index   
            }
            let inner = function () {
                return a
            }
            return inner
        }
        let obj 
        function Closure() {
            obj = test()
        }
    </script>        
</body>
</html>

性能监控效果

聊下内存泄露

dom泄露

js和dom使用的引擎不同,因此我们需要明确js操作dom的时候是非常损耗性能的,这也是为什么很多前端框架使用虚拟节点。 日常开发过程中我们往往会通过js去缓存dom节点信息,比如:

var main = document.querySelector('.main')

当我们完成这一步之后其实这个节点信息就缓存到window下的main里面了。 所以即便我们进行了移除等操作,这个缓存的节点其实依然存在,也不会被回收,导致内存泄露。

测试文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="main">
        <button id="button" onclick="dom()">dom泄露</button>
    </div>
    <script type="text/javascript">
        function dom() {
            var test= document.querySelector('#button')
            var main = document.querySelector('.main')
            main.removeChild(test)
        }
    </script>
</body>
</html>

性能监控效果

聊下内存泄露