文章

JVM - CMS GC

CMS

JDK8默认GC为Parallel Scavenge(新生代)+ Parallel Old(老年代),CMS能和ParNew/Serial GC一起使用。

CMS 是 Concurrent Mark Sweep 的简称,中文翻译为并发标记清除,它的目标是减少垃圾回收时应用线程的停顿时间,并且实现应用线程和 GC线程并发执行。

CMS收集器是用于老年代的垃圾回收,它使用的是标记-清除算法。可以通过 -XX:+UseConMarkSweepGC 参数来显示启动 CMS收集器。

在 CMS之前的 4款收集器(Serial,Serial Old,ParNew,Parallel Scavenge) ,应用线程和 GC线程无法并发执行,当 GC线程工作时必须 Stop The World(将应用线程全部挂起), 并且这 4款回收器关注的是可控的吞吐量,而 CMS收集器,应用线程和 GC线程可以并发执行,目标是缩短垃圾回收时应用线程的停顿时间,这是 CMS和其它 4款收集器本质上的区别,也是它作为里程碑的一个标志。

判断对象是否存活

  • 引用计数法

  • 可达性分析

GC Root选择

1. 虚拟机栈中的引用对象:每个线程的虚拟机栈中的局部变量表中的引用。这些引用可能是方法的参数、局部变量或临时状态。

2. 方法区中的类静态属性引用对象:所有加载的类的静态字段。静态属性是类级别的,因此它们在整个Java虚拟机中是全局可访问的。

3. 方法区中的常量引用对象:方法区中的常量池(例如字符串常量池)中的引用。

4. 本地方法栈中的JNI引用:由 Java本地接口(JNI)代码创建的引用,例如,Java代码调用了本地 C/C++库。

5. 活跃的 Java线程:每个执行中的Java线程本身也是一个GC Root。

6. 同步锁持有的对象:被线程同步持有的对象。

7. Java虚拟机内部的引用:比如基本数据类型对应的Class对象,一些常见的异常对象(如NullPointerException、OutOfMemoryError)的实例,系统类加载器。

8. 反射引用的对象:通过反射API持有的对象。

9. 临时状态:例如,从Java代码到本地代码的调用。

GC root如何被枚举

作为 GC Roots的对象类型有很多种,遍及 JVM中的多个区域,对于现如今这种大内存的 VM,如果需要临时去扫描各区域来获取 GC Roots,那将是很大的一个工程量,因此,JVM采用了一种名为 OopMap(Object-Oriented Programming Map)的数据结构,它用于在垃圾收集期间快速地定位和更新堆中的对象引用(OOP,Object-Oriented Pointer)。

OopMap是在 JVM在编译期间生成的,主要作用是提供一个映射,通过这个映射垃圾收集器可以知道在特定的程序执行点(如safepoint)哪些位置(比如在栈或寄存器中)存放着指向堆中对象的引用,这样就可以快速定位 GC Roots。

跨代引用

1.老年代回收的时候同样存在跨代引用的问题,所以在CMS中同样也用卡表维护了记忆集,维护卡表使用了内存写屏障。详细的介绍在G1中已经介绍过了。

CMS回收过程

从整体上看,CMS 回收垃圾主要包含 5个步骤:

1. Initial Mark(初始标记)

2. Concurrent Marking(并发标记)

3. Remark(重新标记)

4. Concurrent Sweep(并发清除)

5. Resetting(重置

初始标记

初始标记阶段会 Stop The World(STW),即所有的应用线程(也叫 mutator线程)被挂起。该阶段主要任务是:枚举出 GC Roots以及标识出 GC Roots直接关联的存活对象,包括那些可能从年轻代可达的对象。

并发标记

这里的并发是指应用线程和 GC线程可以并发执行。

在并发标记阶段主要完成 2个事情:

1. 遍历对象图,标记从 GC Roots可以追踪到所有可达的存活对象;

2. 处理并发修改

因为应用线程仍在继续工作,因此老年代的对象可能会发生以下几种变化:

a. 新生代的对象晋升到老年代;

b. 直接在老年代分配对象;

c. 老年代对象的引用关系发生变更;

为了防止这些并发修改被遗漏,CMS 使用了后置写屏障机制,确保这些更改会被记录在“卡表”中,同时将相应的卡表条目标记为脏(Dirty),以便后续处理。

  • 记忆集的生成流程分为以下几个步骤:

1、通过写屏障获得引用变更的信息。

2、将引用关系记录到卡表中,并记录到一个脏卡队列中。

3、 JVM中会由Refinement 线程定期从脏卡队列中获取数据,生成记忆集。 不直接写入记忆集的原因是避免过多线程

并发访问记忆集

  • SATB

防止出现并发标记出现可达性标记错误,把不该回收的对象给回收

重新标记

重新标记阶段也会 Stop The World,即挂起所有的应用线程,该阶段主要完成事情是:

1. 并发预清理:在重新标记阶段之前,CMS可能会执行一个可选的并发预清理步骤,以尽量减少重新标记阶段的工作量。(该过程在很多文章中会单独成一个大步骤讲解)

2. 修正标记结果:由于在并发标记阶段导致的并发修改,导致漏标,错标,因此需要暂停应用线程(STW),确保修正这些标记结果。

3. 处理卡表:检查并发标记阶段修改的这些脏卡,并重新标记引用的对象,以确保所有可达对象都被正确识别。

4. 处理最终可达对象:处理那些在并发标记阶段被识别出的“最终可达”(Finalizable)对象。这些对象需要执行它们的 finalize方法,finalize方法可能会使对象重新变为可达状态。

5. 处理弱引用、软引用、幻象引用等:处理各种不同类型的引用,确保它们按照预期被处理。例如,弱引用在 GC后会被清除,软引用在内存不足时会被清除,而幻象引用则在对象被垃圾收集器回收时被放入引用队列。

并发清除

这里的并发也是指应用线程和 GC线程可以并发执行,并发清除阶段主要完成 2个事情:

1. 清除并发标记阶段标记为死亡的对象;

2. 并发清除结束后,CMS 会利用空闲列表(free-list)将未被标记的内存(即垃圾对象占据的内存)收集起来,组成一个空闲列表,用于新对象的内存分配;

CMS缺点

1. 浮动垃圾

在并发清除阶段,因为应用线程可以并发工作,可能会产生垃圾,这些垃圾在当前 GC无法处理,需要到下一次 GC才能进行处理,因此,这些垃圾就叫做“浮动垃圾”。

2. 并发失败

JDK5 默认设置下,当老年代使用了68%的空间后就会被激活 CMS回收,从JDK 6开始,垃圾回收启动阈值默认提升至92%,我们可以通过 -XX:CMSInitiatingOccupancyFraction 参数自行调节。

如果阈值是 68%,可能导致空间没有完全利用,频繁产生 GC,如果是92%,又会更容易面临另一种风险,要是预留的内存无法满足程序分配新对象的需要,就会出现一次 Concurrent Mode Failure(并发失败),因此会引发 FullGC。

这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。

3. 内存碎片

因为 CMS采用的是标记-清理算法,当清理之后就会产生很多不连续的内存空间,这就叫做内存碎片。如果老年代无法使用连续空间来分配对象,就会出发 Full GC。为了解决这个问题,CMS收集器提供了 -XX:+UseCMS-CompactAtFullCollection 参数进行碎片压缩整理,参数默认是开启的,不过 从JDK 9开始废弃。

License:  CC BY 4.0