介绍

CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

在启动JVM参数加上-XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。

CMS采用的基础算法是:标记–清除

CMS过程

1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)

3次标记,1次清除。

初始标记、重新标记这两个步骤仍然需要“Stop The World”。(1、3要stw,2、4都是并发操作,没有stw)

触发条件

1. 周期性GC

由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。

  • 如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发
  • 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%。
  • 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled。
  • 新生代的晋升担保失败。

2. 主动触发

  • YGC过程发生Promotion Failed,进而对老年代进行回收
  • 比如执行了System.gc(),前提是没有参数ExplicitGCInvokesConcurrent

CMS的缺点

1、产生空间碎片:CMS采用的是标记–清除。CMS不会压缩内存碎片,经过CMS收集的堆会产生空间碎片。
2、需要更多的CPU资源:为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切换是不靠谱的。
3、需要更大的堆空间:因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。

4、CMS只回收老年代:年轻带只能配合Parallel New或Serial回收器;

CMS的使用场景

如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(硬件好),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。

晋升失败和并发模式失败

CMS大部分的收集过程可以和用户线程并发执行,大大降低应用的暂停时间,不过也会带来负面影响,在收集完 old gen 之后,CMS并不会做整理过程,会产生空间碎片,如果这些碎片空间得不到利用,就会造成空间的浪费,整个过程中可能发生 concurrent mode failure,导致一次真正意义的Full GC,采用单线程对整个堆(young+old+perm) 使用MSC(Mark-Sweep-Compact)进行收集,这个过程意味着很慢很慢很慢,而且这个碎片问题是无法预测的.

只有CMS算法中才会出现concurrent mode failed,一旦它的出现,意味着有一次耗时很长的FGC了

  • promotion failure(晋升失败):年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的,会去触发Full GC。
  • concurrent mode failure(并发模式失败):并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。

解决办法就是要让年老代留有足够的空间,以保证新对象空间的分配。

cms java 收集器 源码 cms收集器原理_cms

问题

(1)CMS 是如何降低GC延时提高性能呢?

整个CMS垃圾回收过程初始标记重新标记最耗时,因为他们都会STW。

但是,在初始标记阶段 到 最终重标记阶段 之间,基本上中间所有阶段的努力都是并发执行的,且都是为了让重标记阶段的时延成本降到最低

(2)什么时候会触发Stop the world

第一步初始标记
第五步重新标记
会Stop the world

(3)为什么需要Stop the world

当虚拟机完成两次标记后,便确认了可以回收的对象。但是,垃圾回收并不会阻塞我们程序的线程,他是与当前程序并发执行的。所以问题就出在这里,当GC线程标记好了一个对象的时候,此时我们程序的线程又将该对象重新加入了“关系网”中

虚拟机的解决方法就是在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。

(4)CMS参数配置有哪些?

-XX:+UseConcMarkSweepGC
该标志首先是激活CMS收集器。默认HotSpot JVM使用的是并行收集器。

-XX:+CMSClassUnloadingEnabled
相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置该标志

-XX:+CMSParallelRemarkEnabled
采用并行标记方式降低停顿。

-XX:+CMSConcurrentMTEnabled
当该标志被启用时,并发的CMS阶段将以多线程执行

-XX:+UseCompressedOops
这个参数用于对类对象数据进行压缩处理,提高内存利用率。

-XX:MaxGCPauseMillis=50
这个参数用于设置GC暂停等待时间,单位为毫秒,不要设置过低。

一般推荐使用如下的参数设置:

-XX:+UseCompressedOops
-XX:MaxGCPauseMillis=50
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:+CMSClassUnloadingEnabled

(5)什么时候会压缩内存碎片?

CMS不会压缩内存碎片,但会做一些优化,比如会合并老生代中相邻的free空间。

(6)CMS回收日志打印

(7)如何解决内存碎片问题?

Sun官方给出了以下的四种解决方法:
1、增大Xmx或者减少Xmn
2、在应用访问量最低的时候,在程序中主动调用System.gc(),比如每天凌晨。
3、在应用启动并完成所有初始化工作后,主动调用System.gc(),它可以将初始化的数据压缩到一个单独的chunk中,以腾出更多的连续内存空间给新生代晋升使用。
4、 降低-XX:CMSInitiatingOccupancyFraction参数以提早执行CMSGC动作,虽然CMSGC不会进行内存碎片的压缩整理,但它会合并老生代中相邻的free空间。这样就可以容纳更多的新生代晋升行为。

stop the world(STW)的实现原理

1、到安全点再挂起

JVM有个叫做“安全点”和“安全区域”的东西,在发生GC时,所有的线程都会执行到“安全点”停下来。

在需要GC的时候,JVM会设置一个标志,当线程执行到安全点的时候会轮询检测这个标志,如果发现需要GC,则线程会自己挂起,直到GC结束才恢复运行。

2、直接全部挂起再恢复不在安全点的

还有另一种策略是在GC发生时,直接把所有线程都挂起,然后检测所有线程是否都在安全点,如果不在安全点则恢复线程的执行,等执行到安全点再挂起。

但是对于一些没有获得或无法获得CPU时间的线程,就没办法等到它执行到安全点了,所以这个时候只要这个线程是在安全区域的,也可以进行GC,安全区域是一段代码段,在这段代码段中对象的引用关系不会发生变化,所以这个时候进行GC也是安全的