黑马程序员技术交流社区

标题: JavaScript:堆栈溢出&&内存泄漏 [打印本页]

作者: 小江哥    时间: 2019-12-23 19:19
标题: JavaScript:堆栈溢出&&内存泄漏
在JavaScript中,会有听到两个概念:堆栈溢出和内存泄漏,这两种机制在开发中遇到的不多,但是一旦碰到就很头疼。下面就分别来讲述一下二者的概念,触发原因以及解决办法。

堆栈溢出:
什么是堆栈溢出?我们知道JS中的数据存储分为栈和堆,程序代码运行都需要一定的计算存储空间,就是栈了,栈遵循先进后出的原则,所以程序从栈底开始运行计算,程序内部函数的调用以及返回会不停的执行进栈和出栈的操作,栈内被所占的资源也在不断的对应变化,但是一旦你的调用即进栈操作过多,返回即出栈不够,这时候就会导致栈满了,再进栈的就会溢出来。打个比方:就好像是跟女朋友去吃火锅,点了一个鸳鸯锅,俩人开始一人吃辣的一人吃不辣的,很和谐,结果你贪吃,放了很多菜在辣的一边,你又吃不过来,辣油溢出来到不辣的一边,那么你的女朋友就不高兴了,最终你肯定会被骂了。当然了,JS堆栈溢出后不会骂你,但是他会报错然后罢工了。再来看一个网上使用比较多模拟的代码(递归)的例子:
[JavaScript] 纯文本查看 复制代码

function isEven(n) {

    if (n === 0) {

        return true;

    }

    if (n === 1) {

        return false;

    }

    return isEven(Math.abs(n) - 2);

}

当我们打印console.log(factorial(10))答案是true,结果运行也比较快,再看当我们输入console.log(factorial(10000000)),结果是抛出了错误:Uncaught RangeError: Maximum call stack size exceeded(此运行在谷歌浏览器测试),这个错误的意思就是:最大调用超过堆栈大小,这是为什么呢?原因就是,程序在执行代码过程中,需要一定的计算空间即栈,一般大小为1M左右,当你每次调用程序内的函数等其它时,这些就会占用一定的空检,当占用过多时,就会超过该程序所分配的栈的空间,就会报错了。那么,如何解决这个问题?就拿上面的递归例子来说,解决办法如下(前文我们提到了闭包,这里就用闭包和Trampoline(蹦床原理)来解决):
[JavaScript] 纯文本查看 复制代码

function isEven(n) {

    function isEvenInner(n) {

        if (n === 0) {

            return true;

        }

        if (n === 1) {

            return false;

        }

        return function () {

            return isEvenInner(Math.abs(n) - 2);

        }

    }

    function trampoline(func, arg) {

        var value = func(arg);

        while (typeof value === "function") {

            value = value();

        }

        return value;

    }

    return trampoline.bind(null, isEvenInner)(n);

}

内存泄漏
什么是内存泄漏?内存泄漏是指程序被分配的栈内有一块内存既不能使用,也不能被回收。就是你和你女朋友吃火锅,中间有一块位置没有汤,不能烫菜一样的。导致内存泄漏的原因一般有一下几种情况:

函数内未使用声明变量关键字的变量

未销毁的定时器
[JavaScript] 纯文本查看 复制代码
setInterval(function () {

    console.log(1)

}, 1000);

DOM以外的节点引用

var elements = {

    button: document.getElementById('button'),

};

function doStuff() {

    button.click();

}

function removeButton() {

    document.body.removeChild(document.getElementById('button')); // 这时,我们仍然有一个引用指向全局中的elements。button这个节点仍在内存中,不会被回收。

}

闭包的循环引用

function my(name) {

    function sayName() {

        console.log(name)

    }

    return sayName

}

var sayHi= my("tom")

sayHi() //tom

在函数my()内部创建的sayName()函数是不会被回收机制回收,如果闭包不被调用,由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

最后再多加一个概念,我们前文一直提到程序运行时所占的内存空间,并且在程序运行的时候不停的在进行进栈出栈,调用和销毁,这里就涉及到浏览器的垃圾回收机制。什么是垃圾?垃圾就是不再使用的变量,顾名思义,你这个变量没用了,抱歉,你就会被浏览器销毁,以此来腾出空间,浏览器常用的垃圾回收办法有两种:标记清除和引用计数。

标记清除

这个是最常用的方式。当变量进入执行时,会给上一个标记,就证明这个变量进入了代码的执行环境,当变量执行完毕,离开执行环境时,会被标记上离开了执行环境。此时的垃圾回收器会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

引用计数

这种方式用的不多。引用计数,顾名思义就是计算这个变量被引用的次数。当一个变量被声明且赋上引用类型,这个变量的引用次数就会相应的加1,如果包含这个值的引用变量又附上了另外一个值,那么这个值的引用次数就相应的减1,当引用次数变为0的时候,就无法访问这个值了。当垃圾收集器运行的时候,就会回收引用次数为0的值所占的内存,并释放这些内存。






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2