jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

系统性学习JVM请点击JVM学习目录

在前面我们进行了垃圾回收的引入,介绍了如何判断对象是否是垃圾
下面我们就来看看在已经判断好了谁是垃圾的前提下,jvm如何进行垃圾回收。下面主要介绍标记-清除、标记-整理、标记-复制与分代回收这几种算法。

标记-清除算法

首先就是标记-清除算法了,这是四个里面最简单的一个。从其名字我们可以看出,该算法分为两个部分,标记与清除。
首先是标记阶段,通过前面提到过的可达性算法来标记需要回收的对象。
然后是清除阶段,直接将被标记为垃圾的对象清除就完事了。(这里需要注意,所谓清除,并不是说,要把这一段内存的值给置为0,而是直接将这段内存从占用状态改成可用状态,下次需要使用时直接覆盖就可以了)
下面用个示意图来形象的说明一下标记-清除算法(还是word里画的,不会用其他好看的画图工具了555)。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

上图表示堆内存的情况。这里黑色为死亡,灰色为存活,白色为未使用的内存空间。
那么此时进行标记-清除操作,则会将所有黑色的内存区域清除掉,灰色与白色的不动。结果如下图所示。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

如此一来,便将死亡的对象占用的内存空间便被回收。
来说一下该方法的优缺点。

  • 优点:显然可见,该方法十分之简单,而且很高效,很快,因为对比其他方法,都是要标记的,而在清除阶段,也不需要执行什么抹掉内容的操作,只用把要“清除”的内存空间设为可用即可,可以说毫不费力,所以很快。
  • 缺点:缺点也很明显,看上面我们经过标记-清除算法的堆内存空间,可用的区域根本就不连续,假设现在突然要往堆内存中放入一个占5个格子的对象呢?虽然堆内存可用空间足够,但这里却没法放,所以这是个大问题。

标记-整理算法

这个算法实际是对上面的标记-清除算法的一种改进,上面提到标记-清除算法的缺点在于它会导致内存空间不连续,那么这里标记-整理算法就解决这个问题,让经历过垃圾回收的堆内存空间连续。
标记阶段不变,但整理阶段不是直接对可回收对象进行清除,而是让所有存活对象都向内存空间一端移动,然后清除边界以外的内存。
我们还是来通过示意图来看一下。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
上图为标记-整理前,进行标记整理时,jvm会将灰色的都往前移,让黑色的都在灰色后面,效果如下。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
然后以最后一个灰色的为边界,将边界外的内存空间清除掉。形成的效果如下。jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
可以看到,经过标记-整理算法的堆内存变的整整齐齐,干干净净(aabb叠词嘻嘻),也就是内存连续了,这样,就算要放入占用12个格子的大对象我们都能放的下(芜湖)。
好,依然来总结一下优缺点:

  • 优点:显而易见,保证了堆内存的连续,不会浪费堆内存
  • 缺点:首先,要在内存中移动对象,这很慢很低效,并且,在移动时,你还要考虑到并发问题(假如用户线程正在使用你移动的对象怎么办),所以你需要STW,那么此时又更慢了。

由于这些缺点,所以标记-整理的效率不是很高,它比较使用与老年代这种存活多死亡少的垃圾收集,因为死的少大概率意味着移动的也少。但是它的好处也是显而易见的。

标记-复制算法

同样,这个算法也是在标记-清除算法基础上改的。
该算法是这样的,将内存区域分成两个,每次呢只使用其中一个区域,当进行垃圾回收时,将存活的对象复制到另一个区域上(连续存放哦),然后把已经使用过的区域一次性全部清除。
还是来看示意图,(中间那个棍是用来表示分隔两个区域)。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

可以看到,原本的堆内存空间划分成了两个区域,同时只使用一个区域,当进行标记-复制算法时,会将区域1中的灰色对象复制到区域2中,然后将区域1全部清除。结果如下图。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
好,依然来总结一下优缺点。

  • 优点:解决了标记-清除算法内存不连续的问题,可以看到,这里内存是十分连续的,然后,其速度也还是可以的,虽然比不上标记-清除,但还是比标记-整理快很多很多
  • 缺点:缺点也是十分明显,本来挺充足的堆内存空间,现在咔的一下只允许你用一半了,你说难不难受。改算法十分浪费内存空间。

分代回收策略

其实这里说是算法不准确,更多的是一种策略吧,蛮多垃圾收集器都采用的策略。该策略是在分代理论和标记-复制算法上改进得到。
首先,在我们的堆内存中,有很多对象都是很快就死亡的,比如在虚拟机栈顶的局部变量引用的对象,就会很快死亡,诸如此类寿命很短的对象;还有些对象呢,它活的很长,一直在用,经过多次垃圾回收都是存活。显然,堆内存中有这么两种对象,我们在进行垃圾回收时,如果每一次都看看那些寿命长的对象是不是死了,其实是件很低效的事。所以这里分代理论给堆内存中的对象进行的了分类,活的短的对象们统称新生代,活的长的对象统称老年代。jvm针对新生代和老年代划分不同的内存区域,分别进行垃圾回收。

针对新生代对象,因为新生代对象死的块,死的多的特点,所以这里采用标记-复制算法,不过对其进行了改进。具体做法是,将新生代的堆内存空间分成了三个部分:一个较大的Eden空间和两块较小的Survivor空间。Eden:Survivor=8:1。规定同时只有Eden空间与其中一个Survivor空间在使用,而剩下的Survivor不适用。也就是说同一时间,堆内存只有90%在使用,有10%的空间是浪费的。
然后垃圾回收的策略是:平常的对象在堆内存中生成时,一般默认为新生代对象,放入新生代的Eden空间里;当进行垃圾回收时,会对Eden空间和Survivor2空间中的对象进行标记,将存活对象复制到其中一个Survivor1中去,并将这些存活对象的存活年龄+1,(一般当年龄超过一个阈值,就将其放入老年代),然后将Eden空间和Survivor2空间清除;之后再继续使用,当遇到垃圾回收时,会和之前一样将Eden空间的存活对象放到Survivor2中去,并且也会检查Survior1中的对象是否存活,如果存活,也复制到Survivor2中去,然后将Eden空间与Survivor1空间清除,如此往复。
下面,还是用示意图来演示(这里就不遵循8:1来画了,为了节省时间,直接在刚才的图上进行修改了):
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
此时进行对新生代的垃圾回收,要将Eden中所有灰色的对象复制到Survivor1中,其年龄+1,然后对Eden空间和Survivor2空间进行清除。效果如下图:
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

然后继续使用,新创建的对象放在Eden空间中,当放不下了需要垃圾回收时,进行标记,此时Survivor1中的对象也要标记。如下图所示。
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)

此时,将Eden空间和Survivor1中存活对象复制到Survivor2中,其对象年龄+1,并将Eden空间和Survivor1空间清除。结果如下图:
jvm学习 垃圾回收算法详解(标记清除,标记复制,标记整理,分代回收)
以上便是针对新生代的回收策略。

而针对老年代,因为其寿命长,死亡少的特点,所以就一般直接采用标记-整理算法即可。

针对新生代的垃圾回收,我们叫minor gc,针对老年代的垃圾回收,我们叫major gc,而针对新生代和老年代的全局垃圾回收,我们叫full gc。这里针对老年代的gc很少,只有cms收集器用,并且cms也基本用的很少了,所以这里就不讨论major gc了,只考虑minor gc 和fullgc。
当新生代空间不足时,会触发minor gc,此时如果回收不理想,对象在Eden中放不下,则会考虑放在老年代的内存空间中,如果老年代也放不下,此时会触发full gc,在full gc之前,还会先尝试一次minor gc。
当Survivor空间放不下Eden中存活的对象时,会直接将其放入老年代。
这里需要注意,minor gc会STW。

那么下面我们来总结一下分代回收策略的优缺点:

  • 优点:考虑到了对象声明周期的不同,划分为新生代和老年代,从而对新生代回收频繁,对老年代不频繁,从而提高了回收效率,且改进了标记-复制,将内存资源损耗从50%降低到了10%
  • 缺点:无太明显缺点,总体算还行

参考资料

  • 《深入理解jvm》周志明