JS 垃圾回收机制(GC)

lxf2023-05-18 01:41:22

一、JS 垃圾回收机制(GC – garbage collection)

JS 垃圾回收机制是什么?

JS 的垃圾回收机制是为了防止内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。

变量的生命周期

当一个变量的生命周期结束之后它所指向的内存就应该被释放。JS有两种变量,全局变量和在函数中产生的局部变量。局部变量的生命周期在函数执行过后就结束了,此时便可将它引用的内存释放(即垃圾回收) ,但全局变量生命周期会持续到浏览器关闭页面

回收方式主要包括 标记清除 和 引用计数

  1. 标记清除(mark and sweep) :标记清除是一种常用的垃圾回收方法,它的原理是通过标记那些不再使用的对象,然后进行清除。垃圾收集器会从根节点开始遍历所有对象,标记所有能够从根节点访问到的对象,然后将未被标记的对象进行清除。
  2. 引用计数(reference counting) :引用计数是另一种垃圾回收方法,它的原理是通过记录每个对象被引用的次数,当引用次数为 0 时就可以将该对象进行清除。引用计数方法的优点是可以很快地回收那些被引用次数为 0 的对象,但是它也存在一些例如循环引用的问题。

标记清除(mark and sweep)

大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。

垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。

优点:

  1. 容易实现:实现起来相对简单,只需要通过遍历标记对象然后清除未标记对象即可。
  2. 可以很好地处理循环引用的对象:通过从根节点开始遍历所有对象,标记所有能够从根节点访问到的对象,可以很好地处理循环引用问题。
  3. 对于非常短暂的对象可以更快地进行回收:由于标记清除算法是以对象为单位进行垃圾回收的,所以对于非常短暂的对象,这种算法可以更快地进行回收。

缺点:

  1. 对于大量的对象需要遍历整个对象图,会造成较大的性能损失:对于大量的对象,垃圾回收器需要遍历整个对象图,标记所有能够从根节点访问到的对象,这会造成较大的性能损失。
  2. 需要停止所有 JavaScript 的执行:由于垃圾回收器需要遍历整个对象图,标记所有能够从根节点访问到的对象,所以在垃圾回收的过程中,需要停止所有 JavaScript 的执行,这会造成浏览器的卡顿。

引用计数(reference counting)

这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加 1,当这个变量指向另一个时该值的引用次数便减1。当该值引用次数为 0 时就会被回收。

优点:

  1. 可以在对象不再被引用时立即回收内存,相对高效。
  2. 可以很好地处理循环引用的对象。

缺点:

  1. 对于引用计数器为 0 的对象,需要立即进行内存回收,导致频繁的垃圾回收会造成程序执行效率下降。
  2. 引用计数算法需要维护一个引用计数器,这会增加一些开销,而且容易引发循环引用的问题。
  3. 无法处理循环引用中的内存泄漏。

该方式会引起内存泄漏的原因是它不能解决循环引用的问题

function sample(){
    var a={};
    var b={};
    a.prop = b;
    b.prop = a;
}

这种情况下每次调用 sample() 函数,a 和 b 的引用计数都是 2,会使这部分内存永远不会被释放,即内存泄漏。

低版本 IE 中有一部分对象并不是原生 JS 对象。例如,其 BOM 和 DOM 中的对象就是使用 C++ 以 COM(Component Object Model) 对象的形式实现的,而 COM 对象的垃圾收集机制采用的就是引用计数策略。

因此即使 IE 的 JS 引擎是用的标记清除来实现的,但是 JS 访问 COM 对象如 BOM, DOM 还是基于引用计数的策略的,也就是说只要在 IE 中涉及到 COM 对象,也就会存在循环引用的问题。

当一个 DOM 元素和一个原生的 JS 对象之间的循环引用时:

var ele = document.getElementById("eleId");
var obj = {};
obj.property = ele;
ele.property = obj;

添加 obj.property = null; ele.property = null; 即可解除原生 JS 对象与 DOM 元素之间的连接。

当闭包中创建循环引用时:

window.onload = function outerFunction(){
    var obj= document.getElementById("eleId");
    obj.onclick = function innerfunction(){
        console.log(obj.id);
    }
}

上面这个代码创建了一个作为 obj 元素处理程序的闭包,而这个闭包则又创建了一个循环引用。obj 引用了 document.getElementById("element"),而 document.getElementById("eleId") 的 onclick 方法会引用包括 obj 以内的外部环境中的变量,所谓“外部环境”包括了包含函数的整个活动对象,所以一定会包括 obj(即使闭包没有对 obj 进行直接的引用,例如上文程序中没有 obj.id 出现,包含函数的活动对象(obj)中也依旧会保存一个引用)。

可以改成下面这个:

window.onload = function outerFunction(){
    var obj= document.getElementById("element");
    var id = obj.id; // 将 obj 副本保存于变量 id 中,则不会使 obj 元素处理程序的闭包创建循环引用
    obj.onclick = function innerfunction(){
        console.log(id);
    }
    ele = null;//手动断开 obj 对 document.getElemengById("element")的引用
}

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!