码迷,mamicode.com
首页 > 编程语言 > 详细

javascript 内存泄漏的学习

时间:2015-11-05 18:29:49      阅读:333      评论:0      收藏:0      [点我收藏+]

标签:


概念

内存泄漏: 用动态存储分配函数动态开辟的空间,在使用完毕后未释放, 木有任何指针指向他,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收, 占着茅坑不**)即所谓内存泄漏。
等所有内存都被占完之后, 系统就跪了. 
 
内存分配方式

说道内存泄露,就不得不谈到内存分配的方式。内存分配有三种方式,分别是:
  一、静态分配( Static Allocation ):静态变量和全局变量的分配形式。如果把房间看做一个程序,我们可以把静态分配的内存当成是房间里的耐用家具。通常,它们无需释放和回收,因为没人会天天把大衣柜当作垃圾扔到窗外。
  二、自动分配( Automatic Allocation ):在栈中为局部变量分配内存的方法。栈中的内存可以随着代码块退出时的出栈操作被自动释放。这类似于到房间中办事的人,事情一旦完成,就会自己离开,而他们所占用的空间,也随着这些人的离开而自动释放了。
  三、动态分配( Dynamic Allocation ):在堆中动态分配内存空间以存储数据的方式。也就是程序运行时用malloc或new申请的内存,我们需要自己用free或delete释放。动态内存的生存期由程序员自己决定。一旦忘记释放,势必造成内存泄露。这种情况下,堆中的内存块好像我们日常使用的餐巾纸,用过了就得扔到垃圾箱里,否则屋内就会满地狼藉。因此,懒人们做梦都想有一台家用机器人跟在身边打扫卫生。在软件开发中,如果你懒得释放内存,那么你也需要一台类似的机器人——这其实就是一个由特定算法实现的垃圾收集器。而正是垃圾收集机制本身的一些缺陷,导致了javascript内存泄露。
 
 
级块作用域( js 函数内部可以访问外部变量, 而外部不能访问函数里面)

        一, 类c 语言有 级块作用域的概念 
//C语言 
#include <stdio.h> 
void main() { 
    int i=2; 
    i--; 
    if(i) { 
        int j=3; 
    } 
    printf("%d/n",j); // j 未定义
}

 

 
        二, js 没有级块作用域的概念, 只支持函数作用域, 也就是父函数不能访问函数内的变量, 而函数内访问未定义的变量会创建全局变量
var a = 0;
function test() {
    var c = 1;
    alert(b);// 会创建b 这个全局变量
    alert(a);// 可以访问a
}
alert(c);// undefined 不能访问c

 

        注意: 函数内引用 函数外的变量, 会使得该变量一直保存在内存中, 因此任何对该变量的引用都会读取该变量的最终的值
var res = [];
function test() {  
    for(var i = 0; i < 3; i++) {// 我想点击
        res[i] = function () {
            alert(i);
        }
    }
}
res[0]();// 会一直输出2 , 因为i 一直在内存中, 没被回收
res[1]();// 会一直输出2 , 因为i 一直在内存中, 没被回收
res[2]();// 会一直输出2 , 因为i 一直在内存中, 没被回收

 

        而要消除这种情况, 可以引用闭包 ,神马是闭包? 在理解闭包以前.最好能先理解一下作用域链的含义,简单来说,作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止.当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined. 
        了解了作用域链,我们再来看看js的内存回收机制,一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了.对应的内存空间也就被回收了.下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用.但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题.如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收.
        也就是说,有了闭包,嵌套的函数结构才可以运作 ( 简单的说就是一个匿名函数, 而闭包内与外部通信, 可以通过匿名函数传参的形式)
function test(){ 
    (function (){ 
        for(var i=0;i<4;i++){ 
        } 
    })(window); // 这里可以往闭包里面传参
    alert(i); // 未定义, 因为i已经被销毁了, 为啥, 因为函数作用域呗(不能访问里面), 这是个匿名函数
} 
test();

 

        貌似很好用, 所以在JS中,为了防止命名冲突,我们应该尽量避免使用全局变量和全局函数。那么,该如何避免呢?不错,正如上文demo所示,我们可以把要定义的所有内容放入到一个
(function (){ 
//内容 
})(window, window.document .....);

 

        中, 这样外部就不能访问这里面的变量了,内存也会自动回收了, 也就大丈夫了, 再看看上面那个例子(=.= 听说面试经常会问这种)
var res = [];
function test() {  
    for(var i = 0; i < 3; i++) {// 我想点击
        (function (j) {
            res[j] = function () {
                alert(j);
            }
        })(i);// 函数结束, j 已经自动回收了, i 只是个参数
    }
}
res[0]();// 0
res[1]();// 1
res[2]();// 2

 

 
 
js 的垃圾回收机制(引用 http://kb.cnblogs.com/page/74836/)

    一, 引用计数 reference counting: 这个可能是最早想到的方法。形象点说,引用计数可以这么理解,房子里放了很多白纸,这些纸就好比是内存。使用内存,就好比在这些纸上写字。内存可以随便使用,但是,有个条件,任何使用一张纸的人,必须在纸的一角写上计数1,如果2个人同时使用一张纸,那么计数就变成2,以此类推。当一个人使用完某张纸的时候,必须把角上的计数减1,这样,一旦当计数变为0,就满足了垃圾回收条件,等在一旁的机器人会立即把这张纸扔进垃圾箱。基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须实时运行的程序。但引用计数器增加了程序执行的开销;同时,还有个最大的问题,这个算法存在一个缺陷,就是一旦产生循环引用,内存就会被泄露。举个例子,我们new了2个对象a和b,这时,a和b的计数都是1,然后,我们把a的一个属性指向b,b的一个属性指向a,此时,由于引用的关系,a和b的计数都变成了2,当程序运行结束时,退出作用域,程序自动把a的计数减1,由于最后a的计数仍然为1,因此,a不会被释放,同样,b最后的计数也为1,b也不会被释放,内存就这么泄露了!
 
当JavaScript代码生成一个新的内存驻留项时(比如一个对象或函数),系统就会为这个项留出一块内存空间。因为这个对象可能会被传递给很多函数,并且会被指定给很多变量,所以很多代码都会指向这个对象的内存空间。JavaScript会跟踪这些指针,当最后一个指针废弃不用时,这个对象占用的内存会被释放。
   A ---------> B ------------> C
 
例如对象A有一个属性指向B,而B也有一个属性指向C。即使当前作用域中只有对象A有效,但由于指针的关系所有3个对象都必须保留在内存中。当离开A的当前作用域时(例如代码执行到声明A的函数的末尾处),垃圾收集器就可以释放A占用的内存。此时,由于没有什么指向B,因此B可以释放,最后,C也可以释放。
然而,当对象间的引用关系变得复杂时,处理起来也会更加困难。
   A ---------> B ------------> C
                         ^、_ _ _ _ _ _ _|
 
这里,我们又为对象C添加了一个引用B的属性。在这种情况下,当A释放时,仍然有来自C的指针指向B。
function leak() {// 循环引用例子
    var a = new Object();
    var b = new Object();
    a.attr1 = b;// a 包含 b 的指针
    b.attr2 = a;// b 包含 a 的指针
}

 

优点: 实现简单
缺点: 容易产生循环引用, 内存泄漏的情况
 
   二, 标记清除 mark sweep: 同样是房间和白纸的例子,这次规则有所修改。白纸仍然随便用,并且,一开始,不需要做什么记号,但是用到某个时候,机器人会突然命令所有人停下来,这时,需要每个人在自己仍然需要使用的白纸上做一个记号,大家都做完记号后,机器人会把那些没有记号的白纸全部扔进垃圾箱。正如其名称所暗示的那样,标记-清除算法的执行过程分为“标记”和“清除”两大阶段。这种分步执行的思路奠定了现代垃圾收集算法的思想基础。与引用计数算法不同的是,标记-清除算法不需要运行环境监测每一次内存分配和指针操作,而只要在“标记”阶段中跟踪每一个指针变量的指向——用类似思路实现的垃圾收集器也常被后人统称为跟踪收集器( Tracing Collector )。当然,标记-清楚算法的缺陷也很明显,首先是效率问题,为了标记,必须暂停程序,长时间进行等待,其次,标记清除算法会造成内存碎片,比如被标记清除的只是一些很小的内存块,而我们接下来要申请的都是一些大块的内存,那么刚才清除掉的内存,其实还是无法使用。解决方案,常见的有2种,一是清楚后对内存进行复制整理,就像磁盘整理程序那样,把所有还在使用的内存移到一起,把释放掉的内存移到一起,如图:
技术分享
但是,这样一来效率就更低了。
  第二种方案是不移动内存,而是按大小分类,建立一系链表,把这些碎片按大小连接并管理起来,(4个字节的内存一个链表,8个字节的内存一个链表……)如果我们需要4个字节的内存,就从4个字节的链表里面去取,需要16个字节,就从16字节的链表里面去取,只有到了一定时候,比如程序空闲或者大块的内存空间不足,才会去整理合并这些碎片。
为什么重点谈mark-sweep算法呢,主要是ie对javascript的垃圾回收,采用的就是这种算法。
 
          优点: 解决了引用计数的部分问题
          缺点: 效率低下为了标记,必须暂停程序,长时间进行等待, 内存碎片多
 
   三, 复制copying:mark-sweep算法效率低下,由此,又产生了一种新的奇思妙想,我们再把规则换一下:还是房间和白纸的例子,这次我们把房间分成左右2部分,一开始,所有人都在左边,白纸仍然随便用,一定时候,机器人又会叫大家停下来,这次不做记号了,你只要带着你还需要的白纸转移到右边去就可以了(相当于把现有的程序复制一份,无法使用的部分自然不会被复制),那些没用的纸自然就剩了下来,然后机器人会把左边所有的垃圾打扫干净(相当于把原先使用的那一半内存直接清空),下次执行垃圾回收的时候采用同样的方式,只不过这次从右边向左边迁移。这种算法的效率奇高,可惜,对内存的消耗太大,尤其是在1960年,内存可比黄金贵多了,直接砍掉一半的内存,显然是无法接受的。
 
优点: 算法高效
缺点: 内存消耗太大
 
 
内存泄漏例子

1 闭包, 在旧版本IE中存在一种难以处理的循环引用问题。
当一个循环中同时包含DOM元素和常规JavaScript对象时,IE无法释放任何一个对象——因为这两类对象是由不同的内存管理程序负责管理的。
    例如
function outerFn() {
    var outerVar = {};
    function innerFn() {
        console.log(‘hello‘);
    }
    outerVar.fn = innerFn;// 造成循环引用, 因为 innerFn 引用了 outerVar
    return innerFn;// 由于返回了内部函数innerFn,而 innerFn 是个闭包, 会读取整个outerFn的内容不释放
}

 

    更常见于dom 事件, 如
$(document).ready(function() {
    var button = document.getElementById(‘button-1‘);
    button.onclick = function() {// 内部函数可以读取外部函数的所有资源, 包括 button, 所以循环引用了
        console.log(‘hello‘);
    };
});

 

    正确解法1 : 消除闭包
function hello() {
    console.log(‘hello‘);
}
$(document).ready(function() {
    var button = document.getElementById(‘button-1‘);
    button.onclick = hello;// 这个就不是内部函数了, 也就不是闭包
});

 

    正确解法2 : 在不再需要的时候将其设置为null
$(document).ready(function() {
    var button = document.getElementById(‘button-1‘);
    button.onclick = function() {// 内部函数可以读取外部函数的所有资源, 包括 button, 所以循环引用了
        console.log(‘hello‘);
        this.onclick = null;// 在不再使用的时候把它设为null, 如本例只需要点击一次
    };
});

 

 
    正确解法3 : 用jquery 
$(document).ready(function() {
    var $button = $(‘#button-1‘);
    $button.click(function(event) {
        event.preventDefault();
        console.log(‘hello‘);
    });
});

 

 
 
 
 
 
 
 
 

javascript 内存泄漏的学习

标签:

原文地址:http://www.cnblogs.com/haili042/p/4940169.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!