JS垃圾回收机制
1. 概述
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
这段话解释了为什么需要系统需要垃圾回收,JS不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:var a = "before";
var b = "override a";
var a = b; //重写a
这段代码运行之后,“before”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。
JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
C#、Java、JavaScript有自动垃圾回收机制,但c++和c就没有垃圾回收机制,也许是因为垃圾回收机制必须由一种平台来实现。在JS中,JS的执行环境会负责管理代码执行过程中使用的内存。
2. 变量的生命周期
当一个变量的生命周期结束之后它所指向的内存就应该被释放。JS有两种变量,全局变量和在函数中产生的局部变量。局部变量的生命周期在函数执行过后就结束了,此时便可将它引用的内存释放(即垃圾回收),但全局变量生命周期会持续到浏览器关闭页面。
3. JS垃圾回收方式
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
JS执行环境中的垃圾回收器怎样才能检测哪块内存可以被回收有两种方式:标记清除(mark and sweep)、引用计数(reference counting)。
3.1 标记清除(mark and sweep)
大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。
垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。
3.2 引用计数(reference counting)
这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。
该方式会引起内存泄漏的原因是它不能解决循环引用的问题:
function sample(){
vara={};
varb={};
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对象之间的循环引用时:
varele = document.getElementById("eleId");varobj = {};
obj.property = ele;
ele.property = obj;
添加 obj.property = null;ele.property = null;即可解除原生JS对象与DOM元素之间的连接。
当闭包中创建循环引用时:
window.onload =function outerFunction(){
varobj= document.getElementById("eleId");
obj.onclick =function innerfunction(){
console.log(obj.id);
}
}
上面这个代码创建了一个作为obj元素处理程序的闭包,而这个闭包则又创建了一个循环引用。obj引用了document.getElementById("element"),而document.getElementById("ele Id")的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")的引用
}
垃圾回收机制怎么知道,哪些内存不再需要呢?
最常使用的方法叫做"引用计数"(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
上图中,左下角的两个值,没有任何引用,所以可以释放。
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
const arr = [1, 2, 3, 4];
console.log('hello world');
上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。
如果增加一行代码,解除arr对[1, 2, 3, 4]引用,这块内存就可以被垃圾回收机制释放了。
let arr = [1, 2, 3, 4];
console.log('hello world');
arr = null;
上面代码中,arr重置为null,就解除了对[1, 2, 3, 4]的引用,引用次数变成了0,内存就可以释放出来了。
因此,并不是说有了垃圾回收机制,程序员就轻松了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。