概述

在之前的一篇文章,解决JVM内存泄漏的问题中,涉及到了GC的问题,为了把GC切底弄清楚,所以把这一块的知识又复习了一下,现在对GC中的CMS算法做下总结,原来计划是上个周周末写这篇文章的,但是因为有其他事情耽误,于是一直拖到了8月份的下旬了。 JVM的垃圾回收算法有很类别,比如:并行垃圾回收、stop-the-world、并发垃圾回收,垃圾回收器可以是这个类别中的的一种或者多种的组合,因为我经常使用的是CMS垃圾回收器,因此我这里详细介绍CSM垃圾回收算法。阅读完本文你会了解如下的内容:

1.什么是CMS,它是为了解决什么问题而出现的?

2.CMS是如何工作的?

3.CMS使用过程中会经常遇到什么问题,我们需要如何避免这种问题?

什么是CMS?它是为解决什么问题而出现的?

CMS的全称为Concurrent Mark Sweep,中文直译为并发标记删除,这个GC是在JDK的7之后才引入的,为什么要引入CMS? 目的是为了解决GC执行时间过长而导致的应用长时间的停顿的问题。那你会问其他的垃圾回收器会导致应用有较长的停顿时间吗? 回答是肯定的,比如:

并发垃圾回收器

每一次GC它都会stop-the-world,而且是新生代和老年代一起执行GC,这样就导致应用的线程都会停止执行,等待GC执行完毕之后,应用的线程才能够执行,因为在老年代的GC中还需要执行空间压缩,对象转移等操作,就导致了每一次GC的时间都会很长;

串行垃圾回收器

每次GC只有一个线程在执行,而且每次执行的时候,应用的线程都需要停止执行。因为只有一个线程执行垃圾回收,这就导致了应用有更长的停顿时间。

如果我们的应用是批处理类型的,在单位时间内追求的是吞吐量,那么并行垃圾回收器可以满足需求,如果我们的应用是客户端类型的,串行垃圾回收器能够满足需求,但是如果我的应用的实时性要求比较高,对于长时间的停顿是无法接受的,那么这两种垃圾收集器就都不能满足需求了。在此背景下,于是就诞生了CMS垃圾回收器。它是如何可以满足低延迟的需求的,下面详细解释。

CMS是如何工作的?

CMS的执行依次遵循如下几个阶段:

  1. 初始标记:在这个阶段,应用会stop-the-world,GC线程会从GC Root出发找出可以被Root对象引用到的对象,这些对象被称为活对象;
  2. 并发标记:和初始标记做一样的事情,应用也会stop-the-world;
  3. 重新标记:在这个阶段里,做的事情和初始阶段是一样的,主要是避免有遗漏的对象没有被标记;
  4. 标记清除:依次从内存的初始位置开始寻找那些没有被标记的对象,进行清除;

那为什么CMS能够满足应用对于低延迟需求?

因为CMS的老年代GC和新生代的GC可以彼此独立进行,如果老年代的GC在执行的时候,发现有新生代的GC要执行,就会停下来,让新生代的GC优先执行,这样应用的线程只需要等待新生代的GC完成后就会恢复执行,不再需要等待老年代的GC执行完成,并行垃圾回收算法里,应用的线程需要等待新生代和老年代GC都执行完成之后,应用的线程才能恢复执行; 初始标记、并发标记都会导致应用停顿,但是这个时间是很短暂的; 在标记清除阶段,CMS是和应用的线程并发执行的,因此应用可以继续执行,不会受到gc的影响;

CMS的垃圾回收器存在的问题以及如何避免类似的问题

  1. 如果我们新对象生成的速度大于旧对象被回收的速度,那么就会导致内存益处的问题
  2. 如果新晋升到老年代的对象太大,内存中没有足够大的连续的空间,就会导致CMS执行一次FULL GC,这样就导致了应用的性能下降
  3. 上述两个问题都不可预测

这几个问题,我们一方面可以通过增加JVM的内存大小,避免内存不够用的问题,另一个方面,可以通过设置如下参数:

-XX: +CMSInitiatingOccupancyFraction

调整何时触发CMS的执行,该值默认是70%,然后设置如下参数,用于打开JVM的日志输出:

-XX:+PrintGC

这样我们就可以观察是否有full gc,当前的触发CMS执行是在什么比例下执行的,通过反复调整,看在什么百分比之下,full gc的频次是最低的。

总结

本文介绍了Java中几种常见的GC算法,包括并行、stop-the-world、并发,因为常用的是CMS垃圾回收算法,因此详细介绍了CMS是因为什么问题而被引入的,以及它和其他几种垃圾回收器的区别,另外介绍了CMS垃圾回收器的工作机制,以及它面临的问题,本文给出了这个问题的解决方案,这个方案是需要不断的进行观察,调试,找到一个最优的配置,以此降低full gc的频次。