天天看点

闲谈JVM(七):浅谈JVM GC之CMS GC前言CMS GC结语

文章目录

  • 前言
  • CMS GC
    • CMS收集执行过程
        • 初始标记
        • 并发标记
        • 并发预清理
        • 重标记
        • 并发清理
        • 并发重置
      • GC ROOT
    • CMS的弊端
    • CMS常用参数
        • -XX:+UseConcMarkSweepGC
        • -XX:+UseParNewGC
        • -XX:ParallelGCThreads=value
        • -XX:CMSInitiatingOccupancyFraction=value
        • -XX:+UseCMSInitiatingOccupancyOnly
        • -XX:+CMSClassUnloadingEnabled
        • -XX:+ExplicitGCInvokesConcurrent
        • -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
        • -XX:+DisableExplicitGC
  • 结语

前言

在上一篇中,我们对JVM的垃圾回收机制进行了整体的概述,了解了JVM中GC的种类, 本篇,我们对其中的一个GC收集器进行详细的探讨,即目前使用最为广泛的GC收集器——CMS GC。

CMS GC

Oralce官方对CMS的定义:

The Concurrent Mark Sweep (CMS) collector is designed for applications that prefer shorter garbage collection pauses and that can afford to share processor resources with the garbage collector while the application is running.

CMS收集器是为那些希望较短的垃圾收集暂停并且可以在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计的。

CMS收集器的主要目标就是:低应用停顿时间。该目标对于大多数交互式应用很重要,比如web应用。

CMS收集器与其他收集器一样,也是面向分代的垃圾收集器,主要面向老年代进行垃圾收集,其尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时执行并跟踪可访问对象,来减少由于主要收集而导致的暂停时间。是可以与用户线程并发执行的垃圾回收器。

在Java 8中,生产环境比较常见的新老生代GC收集器组合是:ParNew + CMS,在新生代的GC收集器中,ParNew也是唯一一个可以与CMS收集器搭配使用的新生代收集器。

CMS收集执行过程

CMS收集器被设计成在大多数时间能与应用程序线程并行执行,仅仅会有一点(短暂的)停顿时间。

GC与应用程序并行的缺点就是,可能会出现各种同步和数据不一致的问题。为了实现安全且正确的并发执行,其收集周期被分为了好几个连续的阶段。

CMS收集器的GC周期由6个阶段组成。其中4个阶段(名字以Concurrent开始的)与实际的应用程序是并发执行的,而其他2个阶段需要暂停应用程序线程。

初始标记

初始化标记阶段,是CMS GC的第一个阶段,也是标记阶段的开始。主要工作是标记可直达的存活对象。为了收集应用程序的对象引用需要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。

主要标记过程

  • 从GC Roots遍历可直达的老年代对象;
  • 遍历被新生代存活对象所引用的老年代对象。
闲谈JVM(七):浅谈JVM GC之CMS GC前言CMS GC结语

并发标记

在该阶段,GC线程和应用线程将并发执行。也就是说,在第一个阶段(Initial Mark)被暂停的应用线程将恢复运行。

并发标记阶段的主要工作是,通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象。

闲谈JVM(七):浅谈JVM GC之CMS GC前言CMS GC结语

由于在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

  • 新生代的对象晋升到老年代;
  • 直接在老年代分配对象;
  • 老年代对象的引用关系发生变更;
  • 等等。

对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

并发预清理

在该阶段将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识。

闲谈JVM(七):浅谈JVM GC之CMS GC前言CMS GC结语

清除Dirty对象的Card标识:

闲谈JVM(七):浅谈JVM GC之CMS GC前言CMS GC结语

重标记

由于第三阶段是并发的,对象引用可能会发生进一步改变。因此,应用程序线程会再一次被暂停以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。

该阶段会进行如下操作:

  • 遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
  • 根据GC Roots,重新标记;
  • 遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

并发清理

所有不再被应用的对象将从堆里清除掉。

并发重置

收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。

一个常见的误解,CMS收集器运行是完全与应用程序并发的。我们已经看到,事实并非如此,即使“stop-the-world”阶段相对于并发阶段的时间很短。

尽管CMS收集器为老年代垃圾回收提供了几乎完全并发的解决方案,然而年轻代仍然通过“stop-the-world”方法来进行收集。对于交互式应用,停顿也是可接受的,背后的原理是年轻代的垃圾回收时间通常是相当短的。

GC ROOT

在CMS收集的过程中,我们提到了GC ROOTS,可以看到CMS的整个收集过程是通过GC ROOTS进行发起的,那么,GC ROOTS是什么,哪些对象可以定义为GC ROOTS呢?

所谓GC ROOTS,或者说Tracing GC的“根集合”,就是一组必须活跃的引用。

例如说,这些引用可能包括:

  • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • JVM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot JVM里的Universe里有很多这样的引用。
  • JNI handles,包括global handles和local handles。
  • (看情况)所有当前被加载的Java类。
  • (看情况)Java类的引用类型静态变量。
  • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)。
  • (看情况)String常量池(StringTable)里的引用。

注意,是一组必须活跃的引用,不是对象。

CMS的弊端

当我们在真实的应用中使用CMS收集器时,我们会面临两个主要的挑战,可能需要进行调优:

  • 堆内存碎片化问题
  • 对象分配率高

CMS收集器并没有任何碎片整理的机制。因此,应用程序有可能出现这样的情形,即使总的堆大小远没有耗尽,但却不能分配对象——仅仅是因为没有足够连续的空间完全容纳对象。当这种事发生后,并发算法不会帮上任何忙,因此,万不得已JVM会触发Full GC。

如果获取对象实例的频率高于收集器清除堆里死对象的频率,并发算法将再次失败。从某种程度上说,老年代将没有足够的可用空间来容纳一个从年轻代提升过来的对象。这种情况被称为“并发模式失败”,并且JVM会执行堆碎片整理:触发Full GC。

当这些情形之一出现在实践中时(经常会出现在生产系统中),经常被证实是老年代有大量不必要的对象。一个可行的办法就是增加年轻代的堆大小,以防止年轻代短生命的对象提前进入老年代。另一个办法就似乎利用分析器,快照运行系统的堆转储,并且分析过度的对象分配,找出这些对象,最终减少这些对象的申请。

CMS常用参数

-XX:+UseConcMarkSweepGC

Use Concurrent Mark-Sweep GC in the old generation.

激活CMS收集器作为老生代的GC收集器。在Java 8中,如果不手动进行指定,默认的老生代收集器为Parallel Old GC,推荐手动开启CMS GC。

-XX:+UseParNewGC

Use parallel threads in the new generation.

激活ParNew收集器作为新生代GC收集器。ParNew收集器也是新生代中唯一可以与CMS收集器搭配使用的GC收集器。

在Java 8,当使用-XX:+UseConcMarkSweepGC时,-XX:UseParNewGC会自动开启。

-XX:ParallelGCThreads=value

Number of parallel threads parallel gc will use.

指定并行GC收集的线程数量。

例如,-XX:ParallelGCThreads=6 表示每次并行垃圾收集将有6个线程执行。

如果不明确设置该标志,虚拟机将使用基于可用(虚拟)处理器数量计算的默认值。

一般建议和CPU个数相等,因为过多的线程数,可能会影响性能。

JVM计算实现细节如下:
unsigned int Abstract_VM_Version::calc_parallel_worker_threads() {
  return nof_parallel_worker_threads(5, 8, 8);
}

unsigned int Abstract_VM_Version::nof_parallel_worker_threads(
                                                      unsigned int num,
                                                      unsigned int den,
                                                      unsigned int switch_pt) {
  if (FLAG_IS_DEFAULT(ParallelGCThreads)) {
    assert(ParallelGCThreads == 0, "Default ParallelGCThreads is not 0");
    // For very large machines, there are diminishing returns
    // for large numbers of worker threads.  Instead of
    // hogging the whole system, use a fraction of the workers for every
    // processor after the first 8.  For example, on a 72 cpu machine
    // and a chosen fraction of 5/8
    // use 8 + (72 - 8) * (5/8) == 48 worker threads.
    unsigned int ncpus = (unsigned int) os::initial_active_processor_count();
    return (ncpus <= switch_pt) ?
           ncpus :
          (switch_pt + ((ncpus - switch_pt) * num) / den);
  } else {
    return ParallelGCThreads;
  }
}
           

-XX:CMSInitiatingOccupancyFraction=value

Percentage CMS generation occupancy to start a CMS collection cycle. A negative value means that CMSTriggerRatio is used.

触发执行 CMS 回收的老生代内存空间占用的百分比,负值表示使用 CMSTriggerRatio 设置的值动态计算。

CMSInitiatingOccupancyFraction的默认值是92%,这个值是动态算出来的,在JVM运行过程中,会动态分析,以此来动态调整CMSInitiatingOccupancyFraction的值,动态控制CMS进行GC的频度。

在Java 8中,该参数值默认为-1,因此来使用CMSTriggerRatio 设置的值动态计算,CMSTriggerRatio的默认值为80。

-XX:+UseCMSInitiatingOccupancyOnly

Only use occupancy as a criterion for starting a CMS collection.

只根据占用情况作为开始执行CMS收集的标准,只有开启了这个参数,CMSInitiatingOccupancyFraction这个参数才会真正生效。

当该标志被开启时,CMSInitiatingOccupancyFraction的值将不会动态调整,而是固定采用设定值,作为老生代GC的触发阈值。

-XX:+CMSClassUnloadingEnabled

Whether class unloading enabled when using CMS GC.

当使用CMS GC时是否启用类卸载功能。

默认情况下,CMS收集器默认不会对永久代(本地元空间)进行垃圾回收。如果希望对永久代进行垃圾回收,可以进行开启。

注意,即使没有设置这个标志,一旦永久代(本地元空间)耗尽空间也会尝试进行垃圾回收,进行Full GC。

-XX:+ExplicitGCInvokesConcurrent

-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

Enables invoking of concurrent GC by using the System.gc() request.

This option is disabled by default and can be enabled only together with the

-XX:+UseConcMarkSweepGC option.

打开此参数后,在做System.gc()时会做background模式CMS GC,即并行FULL GC,可提高FULL GC效率。

该参数在允许systemGC且使用CMS GC时有效。

-XX:+DisableExplicitGC

Ignore calls to System.gc().

开启后,将会禁用 System.gc() 触发FullGC。

默认情况下,该参数为false,可根据具体情况决定是否禁用。

结语

本篇,我们详细探讨了CMS收集器的相关特性,分别分析了CMS的优劣,CMS收集器在Java 8中,在生产环境广泛使用的GC收集器,是一款非常稳定优秀的GC收集器,但是其内存碎片化的问题也是广为诟病。

因此在Java 9中,JVM默认的GC收集器变为了G1收集器,并且Oracle官方文档也明确指出,在后续的版本中,可能会考虑退役CMS收集器,而推荐使用G1收集器,那么视为CMS收集器的继任者G1收集器有哪些特性,我们在下一篇中将会带来G1收集器的特性分析,敬请期待。