面试题:讲一讲在使用CMS垃圾回收器的情况下,FullGC也即是老年代GC的过程,存活对象的标记算法是什么,什么时候会出现漏标的情况?
面试题常规回答
面试者一般都是回答:老年代GC是使用了标记整理算法,先从GCRoot出发将存活的对象标记,然后并发清理所有的垃圾对象,最后还会进行内存碎片整理。
三色标记的只知道是白色、灰色和黑色,是一种并发标记算法。
漏标的情况就不太了解了。
面试题深入剖析
这里其实就是全面问了FullGC的过程了。这里我们先看一下CMS的老年代GC阶段:
- 初始标记(Stop the World)
- 并发标记
- 重新标记(Stop the World)
- 并发清理
接下来我们一步步分析,首先是第一阶段:
初始标记
初始标记其实就是从静态变量、方法栈内的局部变量出发,去标记这些存活的对象,例如:
public class UsersController {
private static UsersService usersService = new UsersService();
public UsersModel get(@PathVariable("id") Integer id, HttpServletRequest request) {
UsersModel model = new UsersModel();
model.setId(id);
return model;
}
}
这里面的 usersService 这个静态变量和执行到get方法内部的model局部变量都是GC Roots标记的起始。这一阶段是会出现 “Stop the World” 的,但是速度很快,小于毫秒级别,大概是微秒级别。
并发标记
这一阶段会让系统线程同时运行,可以继续创建新对象。垃圾回收线程会尽快对已经识别的存活对象进行追踪。也就是对上面的usersService静态变量和model局部变量查找内部引用了哪些对象、或者被哪些对象引用了。这一阶段最耗时,但是也不会对系统造成卡顿影响,只是占用了CPU资源。
这里的标记算法就是三色标记,即使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。
重新标记
因为第二阶段中系统线程会不停地创建新对象和让老对象变成垃圾,所以第三阶段就是进入 “Stop the World”,然后快速将第二阶段中变动的对象进行重新标记。这里有一种情况会出现漏标的现象,产生的条件如下:
- 黑色对象指向了白色对象。
- 灰色对象指向白色对象的引用消失。
如果在重新标记阶段没有对黑色对象进行重新扫描,则会漏标,因为会把白色D对象当作没有新引用指向从而回收掉。
为了解决漏标问题,CMS 采用增量更新方法,关注引用的增加,增加黑指向白的引用的增加,把黑色重新标记为灰色,下次重新扫描属性。
顺便提一下,G1 采用的是关注引用的删除,记录灰指向白的引用消失,当引用消失的时候要把这个引用推到GC的堆栈,保证白还能被GC扫描到。
并发清理
第四阶段会允许系统线程自由运行,然后由垃圾回收线程来清理之前标记为垃圾的对象。这一阶段比较耗时,但是由于是并发运行的,所以只是占用了CPU资源,不会影响业务系统线程。
最后思考一下:为什么会出现内存溢出的情况?
其中一种情况结合上面的FullGC描述就清楚了,CMS在第二和第四阶段中,业务系统产生的新对象容量一下子超过了老年代的内存大小,导致了CMS无法继续运行,所以就内存溢出了。