一、引言
JavaScript具有自动垃圾收集机制,即执行环境会负责管理代码执行过程中使用的内存。
垃圾收集机制的原理:垃圾回收机制就是垃圾收集器周期性地找到不再使用的变量,并释放掉它们所指向的内存。
JS的垃圾回收机制是为了以防内存泄漏。不再用到的内存,没有及时释放,就叫做内存泄漏,即当已经不需要某块内存时这块内存还存在着。通俗点说就是,这个内存该清掉,但是没有被清掉,就造成了内存泄露。
程序的运行需要内存,只要程序提出要求,操作系统就必须供给内存。对于持续运行的服务进程,必须及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
通常有两个用于标识无用变量的策略(垃圾收集方式):标记清除、引用计数。
三、标记清除(js中最常用的垃圾收集方式)
当变量进入环境(如在函数中声明一个变量),就标记这个变量为“进入环境”。当变量离开环境,则将其标记为“离开环境”。
标记清除:垃圾收集器先给存储在内存中的所有对象加上标记,然后去掉环境中的变量和被环境中的变量引用的对象的标记,剩下的被标记的对象就被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
二、引用计数
引用计数的含义是跟踪记录每个值被引用的次数。
引用计数:当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。循环引用指的是:对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用,例如:
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anOtherObject = objectA;
}
// 对象A和B的引用次数都是2,内存得不到回收。
由于低版本IE中的BOM和DOM的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。 因此,即使IE的JavaScript引擎是使用标记清除策略来实现的,但JavaScript访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。
为了解决这个问题,IE9把BOM和DOM对象都转换成了真正的JavaScript对象,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄露现象。
三、V8如何进行垃圾回收
[图片上传失败...(image-2c0212-1597281228800)]
栈内存的回收:
栈内存调用栈上下文切换后就被回收,比较简单。
函数执行完,形成的执行上下文中,没有东西被上下文以外的内容占用,此上下文就会从执行环境栈中移除(释放),如果有被占用,则压缩到栈的底部(没有释放,就形成闭包)。
堆内存的回收:变量 = null
V8的堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长。
新生代内存回收机制:
- 新生代内存容量小,64位系统下仅有32M。新生代内存分为From、To两部分,进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,之后调换From/To,等待下一次回收
老生代内存回收机制
- 晋升:如果新生代的变量经过多次回收依然存在,那么就会被放入老生代内存中
- 标记清除:老生代内存会先遍历所有对象并打上标记,然后对正在使用或被强引用的对象取消标记,回收被标记的对象
- 整理内存碎片:把对象挪到内存的一端
四、管理内存
确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用(解除引用),这一做法适用于大多数全局变量和全局对象的属性。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收(并不意味着解除就自动回收该值所占用的内存)。
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson('Shine');
// 手工解除全局变量globalPerson的引用
globalPerson = null;
五、垃圾收集与闭包
闭包可以避免全局变量的污染,但是如果闭包使用过多,就会使得很多局部变量常驻内存,增加了内存的开销。滥用闭包在IE中可能会造成内存泄露。
没有产生闭包的情况:
当fn()
执行完之后,函数内部的局部变量a以及局部函数就销毁了。则每执行一次,局部变量和局部函数都是重新定义的,执行完毕后,就会被垃圾收集器回收。
即没有闭包的情况下,执行完fn()
,a
会自动释放。
function fn(){
var a = 1;
return function(){
return a++;
}
}
console.log(fn()); // ƒ () { return a++; }
console.log(fn()()); // 1
console.log(fn()()); // 1
产生闭包的情况:
fn函数每次执行,都会形成一个新的环境,这个新的环境被全局变量test
保存下来,test
和子函数建立了引用关系,子函数和父函数中的局部变量又存在引用关系。
由于两个以上存在引用关系的对象,只要有一个是全局的,那么其他的就不会被回收。由于test
是全局的,因此a
不会被释放。
function fn(){
var a = 1;
return function(){
return a++;
}
}
// 在父函数的外部,调用其局部变量,声明一个全局变量test来接收父函数执行后返回的匿名函数
var test = fn();
console.log(test); // ƒ () { return a++; }
console.log(test()); // 1
console.log(test()); // 2, fn每次执行都会形成一个新的环境,也称为闭包环境
当包含闭包的对象成为垃圾对象,即失去引用test = null
,闭包中涉及的变量a
再也没有被引用,闭包死亡。
解除引用后,就会让a
脱离执行环境,以便垃圾收集器下次运行时将其回收。