[JS之作用域和闭包-6]堆栈溢出和内存泄漏

·  阅读 474

内存泄漏(Memory Leak)

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

主要原因

JavaScript 内存泄漏的主要原因在于一些不再需要的引用(Unwanted References)。例如不再需要的闭包、定时器及全局变量等未能及时解除引用。

常见的 JavaScript 内存泄漏

  1. 意外的全局变量

    JavaScript中并未严格定义对未声明变量的处理方式,即使在局部函数作用域中依旧能够定义全局变量,这种意外的全局变量可能会存储大量数据,且由于其是能够通过全局对象例如window能够访问到的,所以进行内存回收时不认为其是需要回收的内存而一直存在,只有在窗口关闭或者刷新页面时才能够被释放,造成意外的内存泄漏。

    //未声明变量在全局环境创建,通过全局对象访问
    function foo(arg) {
        bar = "this is a hidden global variable"
        this.variable = "potential accidental global"
    }
    foo()
    复制代码

    解决方式

    • 使用严格模式("use strict"),严格模式下定义未声明变量在会抛出错误;
    • 减少创建全局变量,如果必须使用全局变量存储大量数据,确保使用完以后把他设置为null或者重新定义。
    //必须要用的情况下
    //手动释放全局变量的内存
    window.bar = null
    delete window.bar
    复制代码
  2. 被遗忘的定时器和回调函数

    不需要setInterval或者setTimeout时,定时器没有被clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。

        <button>开启定时器</button>
        <script>
            function fn1() {
                let largeObj = new Array(100000)
    
                setInterval(() => {
                    let myObj = largeObj
                }, 1000)
            }
    
            document.querySelector('button').addEventListener('click', function () {
                fn1()
            })
        </script>
    复制代码

    这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj

    但是从 Chrome devTools 查看还是能看出内存泄漏。

    原因其实就是因为setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放。

    解决方式

    设置个标记index每次调用timer函数,让它加一,等达到我们想要的次数后,使用clearInterval(timer)停止执行。

    <button>开启定时器</button>
    <script>
        function fn1() {
            let largeObj = new Array(100000)
            let index = 0
    
            let timer = setInterval(() => {
                if(index === 3) clearInterval(timer);
                let myObj = largeObj
                index ++
            }, 1000)
        }
    
        document.querySelector('button').addEventListener('click', function() {
            fn1()
        })
    </script>
    复制代码
  3. 分离的DOM节点

    什么叫DOM节点?假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况:

    <div id="root">
        <div class="child">我是子元素</div>
        <button>移除</button>
    </div>
    <script>
        let btn = document.querySelector('button')
        let child = document.querySelector('.child')
        let root = document.querySelector('#root')
        
        btn.addEventListener('click', function() {
            root.removeChild(child)
        })
    </script>
    复制代码

    该代码所做的操作就是点击按钮后移除.child的节点,虽然点击后,该节点确实从dom被移除了,但全局变量child仍对该节点有引用,所以导致该节点的内存一直无法被释放。

    解决方式

    改动很简单,就是将对.child节点的引用移动到了click事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了。

        <div id="root">
            <div class="child">我是子元素</div>
            <button>移除</button>
        </div>
        <script>
            let btn = document.querySelector('button')
    
            btn.addEventListener('click', function () {
                let child = document.querySelector('.child')
                let root = document.querySelector('#root')
    
                root.removeChild(child)
            })
        </script>
    复制代码
  4. 闭包使用不当

    下面的例子中,在退出fn1函数执行上下文后,该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子。

        <button onclick="myClick()">执行fn1函数</button>
        <script>
            function fn1() {
                let a = new Array(10000) // 这里设置了一个很大的数组对象
    
                let b = 3
    
                function fn2() {
                    let c = [1, 2, 3]
                }
    
                fn2()
    
                return a
            }
    
            let res = []
    
            function myClick() {
                res.push(fn1())
            }
        </script>
    复制代码

    解决方式

    //让不在需要的函数或者变量等于 null
    fn1 = null
    复制代码

    堆栈溢出(stack overflow)

    堆栈溢出在计算机科学中是指使用过多的存储器时导致调用堆栈产生的溢出。堆栈溢出的产生是由于过多的函数调用,导致使用的调用堆栈大小超过事先规划的大小,覆盖其他存储器内的资料,一般在递归中产生。堆栈溢出很可能由**无限递归(Infinite recursion)**产生,但也可能仅仅是过多的堆栈层级。

    产生原因

    • 上溢:栈满时再做进栈必定产生空间溢出。
    • 下溢:栈空时再做退栈也产生空间溢出。
    // 阶乘,若用递归实现,层级不能过深
    const factorial = n => n <= 1 ? 1 : n * factorial(n - 1)
    factorial()
    
    // 斐波那契数列也一样
    const fibonacci = n => n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)
    fibonacci()
    
    //都会产生Uncaught RangeError: Maximum call stack size exceeded错误。
    复制代码

    解决方式

    递归改循环

    优化原理:所有运算均在一个执行上下文中执行,不用生成额外的上下文。

    const factorial = n => {
        let result = 1
        while (n > 1) {
            result *= n--
        }
        return result
    }
    
    const fibonacci = n => {
        let tmp = 1
        let result = 1
        while (n > 1) {
            [tmp, result] = [result, tmp + result]
            n--
        }
        return result
    }
    复制代码

    尾调用优化

    优化原理:函数返回回溯时不需要做任何额外的计算,故可以不用保存函数的入口环境。

    尾调用的优化依赖于语言实现,其本质还是将尾调用优化为循环的实现方式。ES6之前没有对尾调用进行优化,还是会导致调用栈增长。

    const factorial = (n, result = 1) => n <= 1 ? result : factorial(n - 1, n * result)
    
    const fibonacci = (n, prev = 1, cur = 1) => n <= 1 ? cur : fibonacci(n - 1, cur, prev + cur)
    复制代码

参考:

Js中常见的内存泄漏场景

JavaScript常见的内存泄漏

彻底掌握js内存泄漏以及如何避免

JavaScript 内存泄露问题

一文带你了解如何排查内存泄漏导致的页面卡顿现象

作用域与闭包 - 堆栈溢出和内存泄漏的原理及防范

分类:
前端
标签:
分类:
前端
标签: