javascript垃圾回收与内存泄漏

作者: Husbin | 来源:发表于2018-02-02 10:05 被阅读127次

    垃圾回收机制

    自动垃圾收集机制

    • javascript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。即开发人员不需要当心内存使用的问题,所需内存的分配以及无用内存的回收完全实现了自动管理。这种垃圾回收机制的原理:找出那些不再继续使用的变量,然后释放其内存。为此,垃圾收集器会按照固定的时间间隔,周期性地执行这一操作。

    局部变量的生命周期

    • 局部变量只在函数执行的过程中存在。在这个过程中,会为局部变量在栈内存或堆内存上分配相应的空间,以便存储他们的值。然后再函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放他们的内存以供将来使用。

    javascript中的2中垃圾回收机制

    标记清除(Mark-and-sweep)

    • 包括标记阶段和清除阶段。
    • 标记阶段:垃圾回收器创建了一个roots列表。Roots通常是代码中全局变量的引用。javascript中,window对象是一个全局变量,被当作rootwindow对象总是存在,因此垃圾回收器可以检查他和他所有子对象是否存在(即不是垃圾)。所有的roots被检查和标记为激活(即不是垃圾)。所有的子对象也被递归检查,检查从root开始的所有对象,如果是可达的,则打上标记。标记阶段到此结束。
    • 清除阶段:标记阶段完成时,被标记的对象被视为“存活”对象。然后重新进入递归扫描阶段,将没有标记的对象进行回收。这一阶段称为清除阶段。在扫描的同时,还需要将存活的对象的标记清除掉,以便下一次GC操作做准备。

    引用计数(reference counting)

    • 引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则将该引用次数再加1。相反,如果包含对这个值得引用变量又取得了另一个值,则这个值的引用次数就减1。当这个值得引用次数变为0时,则说明没有办法在访问这个值了,因而久可以将其占用的内存空间回收回来。这样,垃圾回收器下次再运行时,会释放那些引用次数为0的值所占用的内存。

    内存泄漏

    • 内存泄漏是指应用程序使用过的内存片段,在不需要的时候,不能返回到操作系统或可用的内存池中的情况。javascript中的内存泄漏同理,当一个值不再需要被引用了,但却无法被垃圾回收器回收的情况,就是内存泄露。

    javascript中3种常见的内存泄漏

    1. 意外的全局变量

    举个例子(普通变量):

    function fun(){
      globalVar = "this is a hidden global variable";
    }
    //在函数体内部定义变量,如果没有使用var声明,则定义的变量是全局变量,即成为window的一个属性
    //而window总是存在,因此不会被回收
    

    再举个例子(由this创建的变量):

    function foo() {
        this.variable = "potential accidental global";
        console.log(this)
    }
    // Foo 调用自己,this 指向了全局对象(window)
    // 而不是 undefined
    foo();
    
    image.png
    • 解决方案:在js文件头部加上(ES5)use strict,使用严格模式,可以避免此类错误的发生。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。

    2.被遗忘的计数器或者回调函数

    举个例子:

    var someResource = getData();
    setInterval(function() {
        var node = document.getElementById('Node');
        if(node) {
            // 处理 node 和 someResource
            node.innerHTML = JSON.stringify(someResource));
        }
    }, 1000);
    //重复计数器并没有使用 clearInterval()终止,导致内存无法回收
    
    • 对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。

    3. 脱离DOM的引用

    • 有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除
    var elements = {
        button: document.getElementById('button'),
        image: document.getElementById('image'),
        text: document.getElementById('text')
    };
    function doStuff() {
        image.src = 'http://some.url/image';
        button.click();
        console.log(text.innerHTML);
        // 更多逻辑
    }
    function removeButton() {
        // 按钮是 body 的后代元素
        document.body.removeChild(document.getElementById('button'));
        // 此时,仍旧存在一个全局的 #button 的引用
        // elements 字典所引用的button 元素仍旧在内存中,不能被 GC 回收。
    }
    

    4.闭包(正常情况下不会造成内存泄漏)

    由于IE9 之前的版本对JScript 对象和COM 对象使用不同的垃圾收集。因此闭包在IE 的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁

    reference -- javascript高级程序设计第三版

    举个例子:

    function closure(){
        var element = document.getElementById("someElement");
        element.onclick = function(){
            alert(element.id);
        };
    }
    
    • 以上代码创建了一个作为element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对closure()的活动对象的引用,因此就会导致无法减少element 的引用数。只要匿名函数存在,element 的引用数至少也是1,因此它所占用的内存就永远不会被回收。

    解决方案:把element.id 的一个副本保存在一个变量中,从而消除闭包中该变量的循环引用同时将element变量设为null。

    function closure(){
        var element = document.getElementById("someElement");
        var id = element.id;
        element.onclick = function(){
            alert(id);
        };
        element = null;
    }
    
    • 因此可以看出,闭包并不会引起内存泄漏,只是由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集,从而导致内存无法进行回收,这是IE的问题,所以正常使用闭包的情况下不会导致内存泄漏。

    reference -- GC的三大基础算法

    reference -- javascript内存泄露及其避免

    闭包会造成内存泄漏吗?

    相关文章

      网友评论

        本文标题:javascript垃圾回收与内存泄漏

        本文链接:https://www.haomeiwen.com/subject/udzjzxtx.html