今天要讨论的问题是,在Major GC的时候,要不要扫描年轻代。
太长不看直接结论:
- Serial,Parallel scavenge,,Parallel old回收老年代的时候还会回收年轻代,Major GC就是Full GC;
- CMS则是有两种模式,在没有设置-XX:+CMSScavengeBeforeRemark的时候,会扫描年轻代,但是只回收老年代;设置之后,会同时回收老年代和年轻代;
- G1比较特殊,它无论处于何种模式下,都不需要扫描别的代,只需要处理一下记忆集;
下面回归正题。
中国人讲话比较直观,一般喜欢用Old GC来取到Major GC。直到有一次老外问我你说的Old GC是什么鬼,我才知道原来他们偏好用Major GC来描述对老年代的GC。所以今天要讨论的问题,实际上是老年代的GC要不要扫描年轻代。当我们问出这个问题的时候,一般都是指我们使用的GC算法是采用对象扫描的方式来进行,而不是采用引用计数来进行的。
理论上来说,是的
回顾一下,堆被划分成老年代和年轻代。垃圾回收的时候就可以选择回收其中的一部分,如只回收年轻代,这被称为Minor GC;如只回收老年代,这被称为Major GC。
其实不论是Minor GC或者Major GC都存在这么一个问题,怎么处理跨代引用?
所谓的跨代引用,就是指一个引用引用了另外一个代的对象。比如说一个老年代对象引用了年轻代对象,或者一个年轻代对象引用了老年代对象。

如图,在回收年轻代的时候,如果不扫描老年代的对象,那么V会被判定为垃圾,这显然是错的。在回收老年代的时候,如果不扫描年轻代,那么S就会被判定为垃圾。
所以,理论上来说,我们不论回收哪一个代,都需要扫描另外一个代,否则就会出现实际上属于存活对象的对象被当成垃圾回收掉了。
而实际上,我只能说这取决于GC的实现。
有些用记忆集来规避扫描另外一个代
一部分人可能知道记忆集是个什么东西。它在HotSpot的几个垃圾回收器上得到了广泛的应用。但是,记忆集只被用于记录老年代对象指向年轻代对象的引用。换言之,就是年轻代对象指向老年代对象的引用并没有被记录下来。

在这种模式下,年轻代的回收避免了扫描老年代的开销。只需要额外从记忆集里面出发扫描年轻代。
之所以不记录年轻代到老年代引用,是因为性能开销。记忆集的条目是通过写屏障来完成的。也就是在内存分配的地方,插入了一段代码来执行记忆集的更新。年轻代的对象创建、回收和引用修改都是频繁出现的事情。所以,如果还记录年轻代到老年代引用,内存的分配就会加上一个不小的开销。
而另外一个考虑的就是,Major GC是很少发生的。老年代的回收频率要比年轻代的回收频率低一个量级。频率低以至于真的扫描年轻代也不是不能接受的事情。
它们真的扫描整个年轻代!
前面提到,记忆集的技术并没有用于记录年轻代指向老年代的引用,所以实际上,一些垃圾回收器,比如CMS就是扫描了年轻代。
如果考虑到,既然我都扫描年轻代了,干嘛我不直接顺便回收一下年轻代呢?这当然是可以的!实际上,JVM有选项可以控制:
-XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark
使用这两个选项,会促使Major GC和Full GC之前,先执行一次Minor GC。
但是,Serial,Parallel scavenge,,Parallel old这几个回收器,不仅仅是扫描了年轻代,而且还回收了年轻代。其实也就是对于这几个回收器而言,Major GC = Full GC。
独特的G1垃圾回收器
G1又要不同。G1除了分代以外,还把堆划分成了不同的region。所以实际上,G1回收器使用记忆集记录的是region之间的引用。比如regionA会有一个记忆集,记录了所有的别的region里指向它的引用。不论这些region是老年代的,还是年轻代。
总结
所以我们的答案就是看垃圾回收器的实现。
- Serial,Parallel scavenge,,Parallel old回收老年代的时候还会回收年轻代,Major GC就是Full GC;
- CMS则是有两种模式,在没有设置-XX:+CMSScavengeBeforeRemark的时候,会扫描年轻代,但是只回收老年代;设置之后,会同时回收老年代和年轻代;
- G1比较特殊,它无论处于何种模式下,都不需要扫描别的代,只需要处理一下记忆集;

网友评论