java gc回收算法_GitHub - firethewhole/GCAlgorithms: Java垃圾回收算法的简易模拟实现

GCAlgorithms

目录

标记阶段具体过程

清除阶段具体过程

分配

合并

优点

缺点

复制算法具体过程

优点

缺点

标记阶段

压缩节点

优点

缺点

年龄

新生代对象与老年代对象

堆的分配

记录集

分配

新生代GC

老年代GC

优点

缺点

1.Mark_Sweep:GC标记-清除法

GC 标记 - 清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用。

标记阶段具体过程:

首先标记根直接引用的对象,然后递归标记通过指针数组能访问到的对象。为了避免重复标记,会在标记的对象obj头部进行一个标志位设置,通过obj.mark可以访问。如果obj.mark为真,则对象已经被标记过;如果obj.mark为假,则对象没有被标记。标记完所有活动对象,标记阶段就结束了。标记阶段所花费的时间 与活动对象的大小成正比。总结一下,标记阶段就是“遍历并标记”的过程。

清除阶段具体过程:

清除阶段,我们使用一个叫sweeping的对象遍历堆,具体就是从堆的首地址开始,按顺序遍历对象的标志位,也就是我们前面在对象头设置的标志位mark。如果遍历到的对象mark为真,说明是活动对象,不进行回收,但我们会将对象的mark置为false,以便下一次的垃圾回收。为了将非活动对象进行回收,回收就是将对象进行分块,我们使用一个“空闲链表”将这些分块进行连接,之后分配对象只要遍历这个链表就可以找到分块。

那原来的非活动对象怎么处理呢?

在有新对象写入时就将原来的非活动对象进行覆盖了,因为是非活动对象,重写它的域也是没关系的。

分配

分配就是指将垃圾回收再利用。在清除阶段,已经将垃圾对象连接到了空闲链表,搜索空闲链表并寻找大小合适的分块,这个过程就叫分配。

寻找分块又分为First-fit,Best-fit,Worst-fit。

First-fit就是遍历空闲链表找到第一个大于或等于要分配对象大小的分块,如果分块大小正好相当,返回这个分块;如果大于分块大小,将分割成和要分配对象大小的分块和剩下大小的分块,返回大小相等的分块,将剩余大小分块返回空闲链表。

Best-fit就是在空闲链表寻找大小最合适的分块,这意味着要遍历完整个空闲链表。

Worst-fit就是在空闲链表寻找最大的分块,同样要遍历完整个空闲链表。这个方法主要是想将分块切割后的大小最大化,但实际上会生成大量小的分块,不推荐。

综上,当使用单纯的空闲链表时,考虑分配所需的时间,使用First-fit最佳。

合并

从上面可知,分配会产生大量小分块。但如果这些分块是连续的,可以将它们合并起来,变成大的分块。这个“连接连续分块”的过程就是合并。合并过程在清除阶段完成。

优点

实现简单。因为实现简单,所以与其他GC算法结合也相对容易。

与保守式GC算法兼容。在保守式GC算法中,对象是不能移动的,因此保守式GC算法与其他移动对象的算法不兼容(如复制算法、标记-压缩算法),因为标记-清除算法并不会移动对象,所以与保守式GC算法兼容。

缺点

碎片化。由于分配的过程会产生大量小的分块,不久后就会导致无数小的分块散落在堆的各个地方。

分配速度。因为分块是不连续的,每次都需要遍历链表,使用First-fit的话,最坏的情况是每次分配的过程都要遍历到最后一个分块。

与写时复制技术不兼容。在Linux中使用fork()时,大部分内存空间都不会被复制,写时复制技术假装复制了内存空间,实际上是将内存共享。在对共享内存写入时,不能直接重写共享内存。因为其他程序访问时,就会发生数据不一致的情况。重写时,要先复制自己的私有空间,然后重写这个私有空间。复制后只访问这个私有空间,不访问共享内存。而标记-清除算法要重写共享内存,即使没有重写,也会设置标志位,导致频繁的复制。

2.GCCopying: GC复制算法

GC复制算法就是把某个空间里的活动对象复制到另一个空间里,将原空间的对象全部回收。进行复制对象的原空间叫做From空间,复制到的新空间称为To空间。当复制完成后,会将From空间和To空间互换,GC也就结束了。From空间和To空间的大小必须一致,这样才能保证对象的复制有足够空间。

复制算法具体过程:

首先在To空间设置一个free指针,作为复制的起点。之后从根开始搜索,将搜索到的对象进行复制,递归复制其子对象。复制的过程中,检查对象的复制标志位copied,查看是否被复制过,如果copied为true,说明被复制过了,返回新空间对象的地址,这个地址在forwarding指针里;如果copied为false,说明没有被复制过,复制这个对象,然后将copied置为true,返回执行新空间对象的forwarding指针。使用标志位copied还有一个好处,就是如果有多个对象指向同一个对象,那么这个对象只会被复制一次。这里注意一下复制的顺序,是从根的直接引用对象开始复制,然后每复制一个直接引用对象,就将它的子对象进行复制。这样做的好处是有引用关系的对象物理位置相近。

不同于标记-清除算法,复制算法的分配过程比较简单。不需要遍历空闲链表,只需要从free指针开始,申请同样大小的内存空间,也不会造成碎片化。分配完成后,将free指针往后移动申请的内存大小。

优点

优秀的吞吐量。与标记-清除算法相比,复制算法只需要寻找活动对象和复制对象的时间,没有遍历空闲链表的时间,能在较短的时间内完成GC,吞吐量很高。

可实现高速分配。因为分块一个连续的内存空间,只要这个分块大小不小于申请的空间大小,就可以直接分配,然后移动free指针,不需要把时间花在遍历链表上。

不会碎片化。每次复制对象都将对象放在前一个对象后,没有碎片空间,这种行为也叫做压缩。

与缓存兼容。复制后,有引用关系的对象相距较近,而大多数CPU都可以通过缓存读取位置较近的对象。

缺点

堆使用效率低下。因为我们将空间二等分,因此只有一一半的空间存放对象。

不兼容保守式GC算法。前面说过保守式GC算法无法和移动对象的算法兼容。

3.Mark_Compact:GC标记-压缩算法

GC 标记 - 清除算法由标记阶段和压缩阶段构成。

标记阶段

和标记-清除算法的一致,不再赘述。

压缩阶段

压缩阶段具体由三个步骤构成:

设定forwarding指针。用一个scan指针来搜索堆里的活动对象,当搜索到活动对象时,将活动对象的forwarding指针指向new_address,之后new_address根据对象的大小移动位置。

更新指针。由于移动的对象可能会覆盖原来的对象,所以在移动前,需要将forwarding指针更新至移动后的地址。

移动对象。搜索整个堆,将对象移动到forwarding指向的地址,之后将forwarding指针置为null。因为是按堆里原先的顺序移动,所以不用担心对象会被覆盖。

优点

可有效利用堆。标记-压缩算法不会出现复制算法只使用半个堆的情况,可以利用整个堆。

不会产生碎片化。因为使用了压缩,所以没有标记-清除算法的碎片化。

缺点

压缩花费计算成本,吞吐率低。在标记-压缩算法中,总共需要遍历3次堆,耗费时间与堆的大小成正比,而在标记-清除算法中,只需要遍历1次堆。

4.GenerationGC:分代垃圾回收

年龄

大部分对象在生成后马上就变成了垃圾,很少有对象活得很久。利用这个经验,GC引入“年龄”的概念,每经历一次GC,对象的年龄就增长一岁。

新生代对象与老年代对象

刚生成的对象称为新生代对象,到达一定年龄的对象称为老年代对象。新生代的GC称为Minor GC,执行频率高;老年代的GC称为Major GC,频率相对新生代较低。当新生代对象经历一定次数的GC存活后当成老年代对象,这种情况称为晋升。

堆的分配

堆的空间分为4个,分别是生成空间、2个幸存空间和老年代空间。其中,生成空间和2个幸存空间称为新生代空间。新生代对象分配在新生代空间里,老年代空间分配在老年代空间里。在执行新生代GC时,将活动对象复制到一个幸存空间里,这个幸存空间称为From幸存空间,没被复制的空间称为To幸存空间。在下次新生代GC时,将生成空间和From幸存空间的活动对象复制到To幸存空间,之后将From幸存空间和To幸存空间互换。经历一定次数的GC后的活动对象才复制到老年代空间中。

记录集

分代垃圾回收的重点是新生代GC,但考虑这样一种情况,即老年代对象引用新生代空间的对象,这样的对象也是活动对象。但这样一来,还是需要遍历整个堆,大大削弱了分代垃圾回收的优势。于是我们使用记录集来记录老年代对象对新生代对象的引用,这样新生代GC就不需要搜索老年代空间,只需要搜索记录集即可。

这里要注意一点,就是记录集并不是记录新生代对象,而是记录的指向新生代对象的老年代对象。因为因为如果指向新生代对象,当新生代对象晋升时,指向新生代对象的老年代对象无法更新指针,因为记录集里并没有保存指向新生代对象的老年代对象。记录集指向发出引用的对象,当新生代对象晋升时,可以将指针更新到晋升后的老年代对象。

当满足以下三点要求后,即可添加到记录集中:

发出引用的对象是老年代对象

引用指向的对象是新生代对象

发出引用的对象还没被记录到引用集中

分配

首先检查生成空间是否有足够大小的分块,如果没有足够大小的分块,就执行新生代GC。因为执行新生代GC后,生成空间全部都可以利用。如果分配对象大小大于生成空间,可以直接进入更大的老年代空间。

新生代GC

生成空间满了,就会启动新生代GC。首先将根直接引用的活动对象复制到From幸存空间,如果活动对象还没被复制,再检查对象的年龄,如果年龄达到晋升年龄,调用晋升函数;如果还没达到,复制对象及其子对象到From幸存空间。接着,检查记录集的对象,执行子对象的复制。如果子对象复制后还在新生代空间,将标志has_new_obj置为true。之后检查has_new_obj标志,如果为false,说明没有指向新生代的对象,在记录集里删除这个元素。最后将From幸存空间和To幸存空间互换,新生代GC就结束了。

幸存空间满了怎么办?

在复制算法里,From和To空间是二等分的,一定可以保证To空间能容纳From里的活动对象。在分代垃圾回收里,To幸存空间一般比From幸存空间还小,然而要容纳From幸存空间和生成空间里的活动对象,为了稳妥起见,使用老年代空间进行复制。如果频繁发生这种情况,分代垃圾回收的优点就会被淡化,虽然实际上出现地很少。

老生代GC

如果使用复制算法,可用空间会减少,所以一般使用标记-清除算法。

优点

提高了吞吐量。老年代GC是针对难以变成垃圾的老年代对象,回收时间要比新生代GC长,在分代垃圾回收中,当老年代空间不足时,才会执行老年代GC,因此执行频率比新生代GC低。实验表明,分代垃圾回收花费时间是复制算法的1/4。可见吞吐量明显得到了提高。

缺点

不适用所有程序。在对象活得很久的程序使用分代垃圾回收,新生代GC花费时间增多,老年代GC频繁运行,这种情况下,分代垃圾回收反而起到反作用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值