V8垃圾回收机制的理解

一、为什么要有垃圾回收
在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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值