一、为什么要有垃圾回收
在C语言和C++语言中,我们如果想要开辟一块堆内存的话,需要先计算需要内存的大小,然后自己通过malloc函数去手动分配,在用完之后,还要时刻记得用free函数去清理释放,否则这块内存就会被永久占用,造成内存泄露。
但是我们在写JavaScript的时候,却没有这个过程,因为人家已经替我们封装好了,V8引擎会根据你当前定义对象的大小去自动申请分配内存。
不需要手动管理内存,所以自然要有垃圾回收,只分配不回收,内存容易被占满。
垃圾回收
优点:不需要我们去管理内存,把更多的精力放在实现复杂应用上
缺点:不用管理,可能在写代码的时候不注意,造成循环引用等情况,导致内存泄露
二、如何判断是否可以回收
1、标记清除(常用)
(1)垃圾收集器先给存储在内存中的所有变量都加上标记(可以使用任何标记方式)
(2)然后,它会去掉运行环境中的变量以及被环境中变量所引用的变量的标记
(3)剩下的被标记的对象就是无法访问的等待回收的对象
2、引用计数
跟踪记录每个值被引用的次数。
(1)当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。
(2)如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量改变了引用对象,则该值引用次数减1。
(3)当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
(4)这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存
问题=>循环引用:对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用
function foo () {
var objA = new Object();
var objB = new Object();
objA.otherObj = objB;
objB.anotherObj = objA;
}
看如下典型例子:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
这是不是就是一个循环引用呢? el有一个属性onclick引用了一个函数(其实也是个对象),函数里面的参数又引用了el,这样el的引用次数一直是2,即使当前这个页面关闭了,也无法进行垃圾回收。
如果这样的写法很多很多,就会造成内存泄露。我们可以通过在页面卸载时清除事件引用,这样就可以被回收了:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
// ...
// 页面卸载时将绑定的事件清空
window.onbeforeunload = function(){
el.onclick = null;
}
三、V8垃圾回收策略
V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)和老生代(old generation)。
1、新生代内存回收机制
(1)分配方式
新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就可以了,当存储空间快要满时,就进行一次垃圾回收。
(2)算法
新生代采用Scavenge垃圾回收算法(牺牲空间换取时间),在算法实现时主要采用Cheney算法。Cheney算法将内存一分为二,叫做semispace,一块处于使用状态,一块处于闲置状态。处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。
进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,之后调换From/To空间,等待下一次回收。
进行From和To交换,就是为了让活跃对象始终保持在一块semispace中,另一块semispace始终保持空闲的状态。
(3)晋升
当一个对象经过多次复制仍然存活时,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。
对象从新生代移动到老生代的过程叫作晋升。
对象晋升的条件主要有两个:
对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间移动到老生代空间中,如果没有,则复制到To空间。总结来说,如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
2、老生代内存回收机制
(1)回收方式
在老生代中,存活对象占较大比重,如果继续采用Scavenge算法进行管理,就会存在两个问题:
由于存活对象较多,复制存活对象的效率会很低。
采用Scavenge算法会浪费一半内存,由于老生代所占堆内存远大于新生代,所以浪费会很严重。
所以,V8在老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。
(2)算法
老生代主要采用Mark-Sweep(标记清除)和Mark-Compact(标记整理)算法
Mark-Sweep只清除死了的对象
标记清除:老生代内存会先遍历所有对象并打上标记,然后对正在使用或被强引用的对象取消标记,回收被标记的对象
问题:Mark-Sweep最大的问题就是,在进行一次清除回收以后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。
如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
Mark-Compact在标记完存活对象以后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的所有内存。
整理内存碎片:把对象挪到内存的一端
(3)两者结合
在V8的回收策略中,Mark-Sweep和Mark-Conpact两者是结合使用的。
由于Mark-Conpact需要移动对象,所以它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时,才使用Mark-Compact。
3、总结
V8的垃圾回收机制分为新生代和老生代。
新生代主要使用Scavenge进行管理,主要实现是Cheney算法,将内存平均分为两块,使用空间叫From,闲置空间叫To,新对象都先分配到From空间中,在空间快要占满时将存活对象复制到To空间中,然后清空From的内存空间,此时,调换From空间和To空间,继续进行内存分配,当满足那两个条件时对象会从新生代晋升到老生代。
老生代主要采用Mark-Sweep和Mark-Compact算法,一个是标记清除,一个是标记整理。两者不同的地方是,Mark-Sweep在垃圾回收后会产生碎片内存,而Mark-Compact在清除前会进行一步整理,将存活对象向一侧移动,随后清空边界的另一侧内存,这样空闲的内存都是连续的,但是带来的问题就是速度会慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact两者共同进行管理的。
Q.E.D.