神之门V8(2):GC的混乱之治(上)

前面一篇文章介绍了v8中对象是怎么的smart,在这里,我们的对象指针就会变得更smart了,先看看对象在heap中是怎么分类管理的,
下面是V8heap的结构图:

这里写图片描述

对jvm有过了解的伙伴就会发现这玩意很像jvm中的heap结构,而且jdk8中删除了持久带后总体的结构更像了,不错,v8堆正是参考了hotspot jvm的设计,采用对象分代管理的策略,当然不同对象得让GC不同看待,首先是新生代,v8中为js代码新分配的对象基本都会放在这个区,V8采用的Cheney算法中又将这个区分为二等分分别叫做from区跟to区,这个做法很巧妙,待会会说,然后跟jvm类似弄个老生代,其中有一点不同,里面搞了分裂,目的就是将老生代的指针跟他的数据分开,指针区主要是从新生代由于自身的age自增超过阙值而晋升到老生代,而新生代多数都保留着指向其他对象的指针,而且在指针区需要维护一个以指针为边的有向图,这个区域为指针的概率是很大的,所以GC可以有区域针对性地进行废弃指针的回收,数据区出现指针的概率会很低,可能原来的指针被delete掉了,所以对象没有了指向,但有可能被pointer区的指针所指向,其他两个区不是这里的重点,一个放jit编译后生成的代码(一条条指令),相当于一个内存的代码段,气场够大,同样身在heap,你却可以命令我。

问题来了,我怎么知道该把对象判断成指针还是int型呢,v8简单粗暴的通过位标识解决问题,搞出两个位进行类型标记就完成了判别,当然作为分区,边界地址肯定是会有的,至少让我来设计是这样。

看完了heap的结构,再来看看GC是怎么回收对象的,v8中主要使用了Scavenge算法在新生代中进行垃圾回收,而这个算法内部又是调用了Cheney算法,怎么搞呢,首先上面说的,先把新生带分成二等分,

这里写图片描述

首先说说from区跟to区为什么要进行交换,当v8new出一个新对象时比如是个handle时,这个家伙首先被抛到from区,跟许许多多刚分配出来的对象一样,然后这个区很快就满了,这个GC就站了出来,进行一轮指向关系的检查,挑选出幸存者搬到to区,然后from区的残留物全都玩完了,幸存者就算经历过一轮生死考验了,也算有点资历了,所以下一轮GC工作时间他们会晋升到老生带的指针区,跟jvm差不多,可以看下我jvm一篇文章,为什么要这么干呢?我们的内存是连续的,如果假如不移动,GC把废弃的对象回收掉我们的内存分布就会变得千仓百孔,就像上图蓝色的对象杂乱的分布在内存块上,难看是次要,主要是内存碎片太多不利于内存的复用,比如全是1k的内存空洞,就算一个2k的对象请求空间你都得再让GC扫描一下看能不能再释放一点空间,这个时候弱引用的handle就要倒霉了,所以内存碎片我们是不愿意看到的,但二分之一的空白区就解决了这个问题,可以进行内存的搬移,下面结合算法的具体内容说明怎么搬移的,首先我们知道内存的搬移实际上也就是一次擦除与重写,旧的数据去除,再写入新的数据就完成了。

如果以指针为边,上图小块的对象块为节点,就可以在from区构成一个有向图,所以GC被触发时,会进行所有对象的有向图广度优先遍历,入口进行根对象,什么是根对象呢,js的对象都有一个contex执行环境,在引擎空间中就是一个实例,所以对于全局scope来说,dom元素对象就是根对象,根对象总是一个活对象,因为它总是被引擎所引用,比如context,所以全局变量指向的对象跟局部变量指向的对象都是根对象,当然如果闭包中return一个外层scope的局部变量,那这个指向的对象也是根对象了,毕竟全局调用闭包函数是可以得到这个对象的,所以使用js闭包是这里就别放什么大型数组啊,什么多个嵌套属性的对象啦,你觉得这是个函数域的局部变量,离开这个scope就释放,然而事实却不是这样,就相当于内存泄露了,scope实例总是活的,活着其实很简单,只要你有价值即你要被别的对象指向。

遍历的入口为根节点,并且在to区准备两根指针等待幸存者的到来,如图,有向图的遍历需要一个辅助队列,其实巧妙的地方就在于这两个指针替我们完成了这个辅助队列的功能,首先检查A节点,发现他还保留两个指针,指向B和C,所以A,B,C一块被放到to区,bit copy的地址其实就是scanPrt,其实是allocationPtr(这个是内存搬移的分配地址)的值,连续的将三个对象写进这个地址,每写一次allocationPtr会跟着移动,直到指向NULL,最后总是保持在数据的末尾处,然后scanPtr还是停留在A的位置,表示当期才处理完A节点,这个过程其实就是push邻接点到辅助队列的过程,只不过这个队列是连续分布,而且靠移动的指针来从头部移动进行dequeue(移动过程中就相当于dequeue了),而且,注意到连续,不知不觉中,在to区内存的分布变得整齐起来了,因为allocationPtr约束了其实分配地址,就完成了定点整体搬移,还完成了整理,A搞完了,接下来轮到他的邻接点B跟C了,接着下一个循环开始,scanPtr移动到B,然后发现它也有两个指针,指向A跟E,A已经处理过了自然会忽略掉,在对象设个标识字段就可以实现,然后E被搬到allocationPtr的位置,最后to的状态就像图那样,一次类推,scanPtr指向C,并没有发现指针,但它确实是一个活对象,所以再轮到E,有发现它指向C,忽略后,scanPtr就跟allocationPtr重合了,这就是我们GC完成内存搬移的一个重要条件。

搬移完了,并不代表from没有对象了,我们发现刚刚好像有漏掉了两个对象D跟F,因为并没有对象含有指向他们的指针,就是说已经完全失去了利用价值,那就是死对象咯,GC开始进行大清洗了,管你F还有没有其他对象的指针,反正你没有用处了,你的指针要来干什么,赶紧析构了,所以这是个全区性的清理,整个from区就变成什么都没有了,下一个GC周期,这里就变成to区,然后同样的故事还是会发生,如此循环。。

新生代的GC回收到这里就非常清楚了,接下来就到老生带的垃圾回收了,其实算法基本的核心结构是差不多的,下一篇我会用自己的代码的进行说明,新生代的分析就到这里啦~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值