垃圾收集机制

一、引言

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脱离执行环境,以便垃圾收集器下次运行时将其回收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。