javaScript垃圾回收机制

什么是垃圾回收

js具有垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。而在c和c++之类的语言中,开发人员的一项基本任务就是手工跟踪内存的使用情况,这是造成许多问题的一个根源。在边写js程序时开发人员不用在关心内存的使用问题,所需内存的分配以及无用内存的回收完全实现了自动化管理。这种垃圾收集机制的原理其实也很简单:找出那些不再继续使用的变量,然后释放掉其占用的内存。为此垃圾收集其会按照固定的时间间隔(或代码执行中预定的收集时间),周期性的执行这一操作。

变量的生命周期

下面我们来分析一下函数中局部变量的正常生命周期。局部变量只在函数执行过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存中分配相应的内存空间,以便存储它们的值,然后在函数中使用这些变量,直至函数执行结束。此时局部变量就没有存在的必要了,因此可以释放它们的内存一共将来使用。在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易就能得出结论,垃圾收集器必须追踪那个变量有用那个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存,用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两种策略。

标记清除

js中最常用的垃圾收集方式就是标记清除。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为"进入环境"。从逻辑上讲,用远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为"离开环境"。

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量如是进入环境,或者使用一个"进入环境的"变量列表及一个"离开环境的"变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方法)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后在被加上标记的变量将被是为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

IEFireFoxOperaChrome、和SafarijavaScript实现使用的都是标记清除式的垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔略有不同。

引用计数

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

引用计数的问题

引用计数的严重问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。

        function fn() {
             let objectA = new Object();
             let objectB = new Object();
             objectA.b = objectB;
             objectB.a = objectA;
        }

在这个例子中,objectAobjectB通过各自的属性引用;也就是说,这两个对象的引用次数都是2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题。但在采用引用计数的实现中,当函数执行完毕后,objectAobjectB还将继续存在,因为它们的引用次数永远也不会是0。假如这个函数被重复多次调用,就会导致大量内存得不到回收。

ie9版本之前有不一分对象并不是原生javaScript对象。例如,其BOMDOM中的对象就是使用C++COM(组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制就是采用的引用计数策略。因此,即使iejavaScript引擎是使用标记清除策略来实现的,但javaScripr访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中设计COM对象,就会存在引用计数的问题。下面是一个简单的例子:

        let ele = document.getElementById('div');
        let obj = new Object();
        obj.ele = ele;
        ele.myObj = obj;

这个例子在一个DOM元素(ele)与一个原生javascript对象(obj)之间,创造了循环引用。其中,变量obj有一个属性ele属性窒息那个ele对象;而变量ele也有一个属性名myObj指向obj。由于这个循环引用,即使例子中的DOM从页面中移除,他也永远不会被回收。

为了避免类似这样的循环引用问题,最好在不使用的时候手工断开原生javaScript对象与DOM元素之间的链接。例如可以使用下面的代码消除上面的循环引用

        obj.ele = null;
        ele.myObj = null;

将变量设置为null意味者切断变量与它此前引用的值之间的链接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

为了解决上述问题,IE9把DOM和BOM对象都转换成了真正的javaScript对象和。这样就避免了两种垃圾收集算法的并存导致的问题,也消除了常见的内存泄漏的情况

管理内存

使用具备垃圾收集机制的语言编写程序,开发人员一般不必担心内存管理的问题。但是JavaScript在进行内存管理及垃圾收集时面临的问题是有点与众不同的。其中主要的一个问题,就是分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全方面的考虑,目的是防止运行JavaScript的网页耗尽全部系统的内存而导致系统崩溃。内存限制问题不仅回你影响给你变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。

因此确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值蛇追为null来释放其引用---这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在他们离开执行环境时自动被解除引用,如下面这个例子

        function createObj(name) {
            let obj = new Object();
            obj.name = name;
            return obj
        }
        let globalObj = createObj('haha');
        // 手工解除obj的引用
        globalObj = null;

在上面例子中,变量obj取得了createObj()函数返回的值。在createObj()函数内部,我们创建了一个对象并将赋值给obj,然后又为该对象添加了一个name属性。最后,当调用这个函数时,obj以函数返回值的形式赋值给全局变量globalObj。由于obj在函数执行完毕后就离开了执行环境,因此我们无需显式的去为它解除引用。但对于全局变量globalObj,则需要我们在不使用它的时候手工为它清除引用。

不过,解除一个值的引用并不意味者自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行是将其回收。

内存泄漏

是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。

内存溢出

指程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中。

内存溢出与内存泄漏的关系与区别

关系

内存泄露最终会导致内存溢出,由于系统中的内存是有限的,如果过度占用资源而不及时释放,最后会导致内存不足,从而无法给所需要存储的数据提供足够的内存,从而导致内存溢出。导致内存溢出也可能是由于在给数据分配大小时没有根据实际要求分配,最后导致分配的内存无法满足数据的需求,从而导致内存溢出。

区别

内存泄露是由于GC无法及时或者无法识别可以回收的数据进行及时的回收,导致内存的浪费;内存溢出是由于数据所需要的内存无法得到满足,导致数据无法正常存储到内存中。内存泄露的多次表现就是会导致内存溢出。

posted @ 2019-06-26 00:00  我会放电啪啪  阅读(430)  评论(0编辑  收藏  举报