(1)初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing,标记其他未标记的垃圾对象
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容
(4)并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾
在初始标记和重新标记的过程中会发生STW
Q:为什么第二次的STW时间会很长
A:垃圾回收包含对象链采用单链表的形式进行,而单链表如果过长,那么每次的执行时间就会很长。
Q:CMS中为什么初始标记单线程,后续并发执行
A:初始标记的对象为GCRoot和直接相连的对象,而创建线程同时需要资源,从性价比考虑单线程更优;从并发标记开始,进入多线程环境,资源已经开始利用,而重新标记的时候可以直接利用这些被调起的线程,避免空闲浪费。
对于初始标记JDK8以后默认是并行的,可以通过参数 -XX:+CMSParallelInitialMarkEnabled控制
预标记阶段:在并发标记 -》 重新标记 的过程中还存在预标记的过程
有这样一个问题:垃圾对象之间的调用关系,可达性是通过指针指向的,那么也存在young区对象指向Old区对象的可能性。当进行CMS的老年代清理时,怎么识别young对象的指向?
A: 如果通过扫描一次young区对象进行判断,那么整个过程就是全量扫描,效率低(停顿时间变长)。 如果全量扫描不可避免,那么就减少扫描的量,通过执行minor GC减少对象的数量达到这个目的。
并且在执行这个minor GC后记录指向old区对象的信息,以便后续处理。
CMS中存在参数:
CMSScheduleRemarkEdenSizeThreshold 默认值:2M
CMSScheduleRemarkEdenPenetration 默认值:50%
即年轻代垃圾到达2M的时候进入预标记阶段,当eden空间使用到达50%停止进入重新标记;这个过程去完成年轻代的指向信息记录。一次minor GC的发生表示预标记阶段的结束??
由于minor GC不确定何时发生,为了响应速度,默认参数 CMSMaxAbortablePrecleanTime,值为5s,时间结束后直接进入重新标记。
相对应的抛开CMS处理老年代,在回收young区的时候也存在old对象指向young对象的情况,那么肯定也不能全量扫描,但是老年代不像年轻代朝生夕死,占用空间很大,因此引入记忆集的概念:
记忆集:由非回收区域执行回收区域的指针集合;
对应这里的场景:old区执行young区的指针集合就是记忆集
记忆集是逻辑概念,数据结构,在CMS中的具体实现是卡表,采用字节数组的形式:
Q:卡表怎么用?
A:CMS中用byte表示某一块内存地址,每一byte有8位(默认为0),可以表示不同的状态,一旦该区域有对象的指针发生变化,修改这个位置为1,记为脏卡;然后通过脏卡、byte标记位的状态修改指针
Q:什么时候修改指针?哪个阶段要使用卡表?
A:在并发标记阶段,由于业务线程和回收线程的并发操作,可能存在对象引用的变化,此时进行脏卡的标记;在重新标记阶段修改应用,处理脏卡。
指针修改 = 对象应用的修改 =》 并发环境 =》 可以联想到并发标记;并发清除也有可能把,但是应该属于下一次GC
Q:CMS一定是标记清除吗?有一种退化单线程的情况可以说说吗?
A:不一定。可能退化到Serial Old。 CMS包括两种模式:前面说的如四步清理过程属于Background CMS,采用标记清除。
- 这个算法的缺点在于垃圾碎片的产生,当碎片过多的时候空间无法利用,此时并发失败。
- 在并发标记的时候,可能有新垃圾的产生,如果这个时候空间不足了,新晋old对象无法存储,此刻OOM,并发失败。
当上述两种发生的时候,CMS都会退化到Serial Old, 称为Foreground CMS,需要进行Full GC。
Q:CMS中Full GC的发生一定会并发失败的时候发生吗?
A:可以调整阈值。参数:-XX:CMSInitiatingOccupancyFraction -XX:+UseCMSInitiatingOccupancyOnly 的配置可以设定空间占用百分比。到达百分比满足条件后,最后由后台扫描线程扫描后可以提前执行Full GC。默认值92%(1.8版本中,在不同版本会有不同)
计算公式:((100 - MinHeapFreeRatio) + (double)( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
三色标记:用于理解对象引用变化时处理一些特殊情况
黑色:
表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色:
表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色:
表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
流程描述:初始的时候,所用对象为白对象,从GCRoot出发扫描直达对象,如果该对象还有新的指针,标记为灰色,否则为黑色。继续扫描,此时额外判读父节点指针的指向是否全部遍历结束,对其进行判黑。最后回收白色节点(垃圾)。
- 问题:多标和漏标
Q:多标:当部分局部变量(栈中对象)作为GCRoot的时候,方法结束后该对象消失,其直达对象成为浮动垃圾(但是非白);其次,并发标记阶段产生的新垃圾可能不能立刻被标记成白色,这些都是浮动垃圾,也就是多标。
A:解决方案:可以忽略,下一次GC的时候清理即可
Q:漏标:黑当白,将引用对象清除掉,此时可能导致业务逻辑的错误,需要进行解决。
A:增量更新和原始快照。
增量更新:记录每次变化的内容 -> 卡表的使用,记录变化,然后重新标记即可。
原始快照:记录指针引用修改前的状态,