使用G1垃圾收集器
2017 年 01 月 15 日
jvm

    自Oracle JDK 7 update 4之后,JVM中引入了一种新的垃圾收集器--G1垃圾收集器(Garbage-First Collector)。之前的一篇文章,有介绍一些基本的垃圾收集器,比如比较常用的CMS垃圾收集器,但即便是CMS垃圾收集器,其也有一些不足,比如,不能很好地处理内存碎片的问题,随着应用的运行,也会导致内存使用率不高,因此通常在使用CMS垃圾收集器时,会设置相对较大的堆大小。G1垃圾收集器则旨在能够比CMS垃圾收集器更合理地管理内存,或者说更智能地管理内存。本文将介绍有关G1垃圾收集器的基本原理,以便开发人员能考虑在生产中使用之。

  • G1垃圾收集器介绍

  • Garbage-First(G1)收集器是一种服务器(server)类型的垃圾收集器,针对具有大内存的多处理器机器。 大多数情况下,它能满足垃圾收集(GC)暂停时间目标,同时实现高吞吐量,这在之前的垃圾收集器中是无法满足的,从Oracle JDK 7 Update 4之后已完全支持G1垃圾回收器G1垃圾收集器主要为有以下需求而设计:

    类似于CMS垃圾收集器,GC线程可与应用程序线程并行运行
    不想因为压缩内存空间,给GC造成太长的暂停时间
    需要更多可预测的GC暂停持续时间;
    不想要牺牲太多的吞吐量
    不要求太大的Java堆内存

    G1垃圾收集器被计划作为CMS垃圾收集器的长期替代品。相较于CMS,G1的某些特性使其成为更好的解决方案,其一是G1属于压缩垃圾收集器,其没有使用CMS中的空闲内存链表来为对象分配内存,而是使用了区(Region)。这大大简化了垃圾收集器的工作,并且大部分消除了潜在的碎片问题。此外,G1提供了比CMS更可预测的垃圾收集暂停,并允许用户指定所需的暂停目标。

  • G1运行概览

  • 对于一些旧的垃圾收集器(如串行垃圾收集器(Serial)并垃圾收集器(Parallel)CMS垃圾收集器),整个内存堆被分为了三个区域:Eden区Survivor区Old区,如图所示:

    然而,G1采用了不同的堆分布,整个堆会被划分为一些列大小相等且内存连续的区(Region),某些区仍会被标记为旧垃圾收集器中的分代(年轻代老年代永久代),但这些代并没有固定的大小,因此,在内存使用上,具有更大的灵活性,如图所示:

    当执行垃圾回收时,G1会以类似于CMS收集器的方式操作。G1并行执行全局标记阶段,以确定整个堆中存活的对象。 在标记阶段完成后,G1则知道哪些区域大多是空的,它首先在这些区域收集,这通常会产生大量的空闲内存,这就是为什么这种垃圾收集方法称为Garbage-First。 顾名思义,G1将其收集压缩的动作集中在可能充满可回收对象(即垃圾)的区域。G1会使用暂停时间预测模型,来达到用户定义的暂停时间目标,并基于指定的暂停时间目标选择要收集的区域的数量。

    由G1识别为可回收区域中的垃圾对象,会以evacuation(暂且叫淘汰)的方式被回收的,即将存活对象从堆的一个或多个区域复制到堆上的单个区域,并且在此过程中压缩释放这些内存区域,该过程在多处理器上是并行执行的,以减少暂停时间,并增加吞吐量。因此,对于每次垃圾收集,G1将在用户定义的暂停时间内持续运行,以减少内存碎片,这具备了之前的垃圾收集器同时满足这两点的能力(如CMS不执行压缩操作,ParallelOld回收只执行整堆压缩,这会导致相当大的暂停时间)。

    需要注意的是,G1并不是实时垃圾收集器,它只会尽最大可能但不是绝对确定性满足用户设置的暂停时间目标。通过来自先前垃圾收集的数据,G1会估计在用户指定的目标时间内可以收集多少区域。 因此,G1收集器对于收集区域,具有相当准确的的收集成本模型,并且会使用该模型来确定在停留时间目标内应该收集哪个区域和多少区域。

    G1既有并发阶段(与应用程序线程一起运行,例如优化标记清除),也有并行阶段(多线程,例如stop-the-world)。FullGC仍然是单线程的,但如果调整得当,你的应用程序应该避免FullGC

  • 建议使用G1的场景

  • G1为运行在大堆上但又要求较低的GC延迟的应用提供了解决方案,这意味着堆大小约为6GB或更大,并且稳定和可预测的暂停时间低于0.5秒。如今使用CMSParallelOldGC垃圾收集器的应用如果具有一个或多个以下特性,可以考虑切换到G1

    FullGC持续时间太长或太频繁;
    对象分配率晋升率差异较大;
    不希望太长的GC或压缩时间(超过0.5秒或1秒)。
  • G1垃圾收集器如何工作

  • G1堆结构

  • G1中,整个堆会被分割为许多固定大小(在JVM启动时,会指定区数量(2000左右)及大小(1~32M))的内存区(Region)

    如上所述,G1中的区域会被逻辑分为Eden区Survivor区Old区

    如图,不同颜色代表的区域所属于的角色,存活对象将从一个区域被复制或移动到另外的区域,而原区域中的将并行地以非stop-the-world的方式被回收。除了上述的三种逻辑区外,还有一种区用于存储大对象(通常大小大于单个区域的50%),它们被存储在连续的区中,最终这种区域会变为堆中未使用的区域(收集大对象通常不会被优化,因此应用程序应避免创建大对象)。

  • G1中的年轻代

  • G1堆会被分为大约2000个区域(Region),区域大小最小为1M,最大为32M,蓝色区域为老年代,绿色区域为年轻代,如图所示(这些区域可以是不连续的):

  • G1中的Young GC

  • G1中执行Young GC时,会将存活对象移动或复制到一个或多个Survivor区,若对象年龄达到了阈值,则会被晋升至老年代。该过程是Stop-The-World的,为了下一次Young GCEden区Survivor区的大小会根据垃圾收集统计信息用户定义的暂停时间目标等计算得来,这种方式使得调整区域大小十分容易,并按需对区域进行扩展或缩小。

    在存活对象被复制到其他区域或老年代后,堆分布会如下:

  • G1中Old GC的几个阶段

  • CMS一样,G1也是旨在以低暂停时间地收集老年代对象,下表中描述了G1中Old GC的几个阶段(一部分阶段也属于Young GC):

    阶段 描述
    初始化标记
    (Stop-The-World)
    该阶段会随着Young GC一起,并且对Survivor区进行标记(作为根引用区,因为该区内可能有引用指向老年代)。
    扫描根引用区 该阶段会扫描Survivor区中针对老年代的引用,会在应用程序运行时发生,该阶段必须在Young GC前完成。
    并发标记 标记整个堆中存活的对象,该阶段会与应用程序并行,并且可以被Young GC中断。
    重标记
    (Stop-The-World)
    标记堆中存活的对象,并且使用比之前CMS更快的算法snapshot-at-the-beginning (SATB)
    清理
    (Stop-The-World & 并发)
    确定存活对象及完全空闲的区域(Stop-The-World);
    清除Remembered Sets(Stop-The-World);
    重置空闲区域,并归还给空闲列表(并发)。
    复制
    (Stop-The-World)
    将存活对象复制到未使用的区域。该阶段可以在年轻代区域完成(日志记录为[GC pause (young)]),也可以在混合的老年代区域年轻代区域(日志记录为GC Pause (mixed))。
  • G1中Old GC的过程

  • 上面介绍了G1中Old GC的几个阶段,这里再通过图示来阐述下具体的过程。

  • 初始化标记阶段

  • 存活对象的初始化标记是随Young GC一起的,在日志GC pause (young)(inital-mark)中可以看出:

  • 并发标记阶段

  • 该阶段会标记一些空的区域,并在重标记阶段被移除。同时,一些统计信息将用于计算区域活跃度

  • 重标记阶段

  • 该阶段空区域被删除和回收,并对所有区域作活跃度计算

  • 复制/清理阶段

  • 该阶段G1会选择活跃度最低的区域,这些区域收集得最快,这些区域会在Young GC的同时被回收,在日志[GC pause (mixed)]可以看出,因此,年轻代老年代会同时被收集:

  • 复制/清理阶段之后

  • 该阶段将被选择的区域收集,并将存活对象压缩成深蓝色区域和深绿色区域:

  • G1相关的命令行参数及最佳实践

  • 使用G1垃圾收集器,有一些关键的命令行:

    -XX:+UseG1GC:开启使用G1垃圾收集器;
    -XX:MaxGCPauseMillis=200:设置最大暂停时间目标,这并不是一个绝对的目标,JVM会尽量保证达到该目标,默认为200毫秒;
    XX:InitiatingHeapOccupancyPercent=45:整个堆占用比达到多少后,G1启动并发GC周期
  • 最佳实践

  • 在使用G1时,有一些可供参考的最佳实践,下面将作相关介绍。

  • 不要设置年轻代大小

  • 设置年轻代大小(-Xmn)后,将会影响G1的一些默认行为,如:

    G1不再考虑暂停时间目标,也就是说,设置年轻代大小会禁用暂停时间目标
    G1不再能按需扩展或收缩年轻代大小。
  • 响应时间指标

  • 不要使用平均响应时间(ART)作为度量,来设置XX:MaxGCPauseMillis = N,而是只会在90%以上的时间能达到该目标值。 这意味着发起请求的用户中,有90%不会遇到高于目标的响应时间。记住,暂停时间是一个目标,并不能保证始终能达到。

  • 什么是Evacuation失败?

  • 在GC期间(Survivor区和晋升的对象),若JVM堆区域耗尽,则会发生晋升失败(Promotion Failure)。由于堆达到了最大值,而不能进行扩展,这会在启用GC日志(XX:+PrintGCDetails)时,通过日志to-space overflow体现,这会造成严重的影响,如以下的一些步骤:

    GC仍然需要继续,内存空间必须得到释放;
    未成功复制的对象必须被晋升到适当的地方;
    任何对CSet集中的Rsets区域的更新必须重新生成。
  • 如何避免Evacuation失败

  • 开发人员可考虑通过以下选项,来避免Evacuation失败

    增加堆大小:增加-XX:G1ReservePercent=n(默认为10),在需要更多空间的情况下,G1会保留一些空闲内存,提前提示空间不足;
    提前启动标记周期
    增加并发标记的线程数-XX:ConcGCThreads=n
  • G1相关命令行参数列表

  • 命令行选项 默认值 描述
    -XX:+UseG1GC 禁用 启用G1垃圾收集器。
    -XX:MaxGCPauseMillis=n 200ms 设置最大的GC暂停时间,JVM会尽量满足该目标。
    -XX:InitiatingHeap OccupancyPercent=n 45 当整个堆的占用率达到该值时,将启动并发GC周期
    -XX:NewRatio=n 2 老年代/年轻代内存占比。
    -XX:SurvivorRatio=n 8 Eden区/Survivor区内存占比。
    -XX:MaxTenuringThreshold =n 15 用于控制对象经历多少次Minor GC后,晋升到老年代。
    -XX:ParallelGCThreads=n 取决于不同平台 垃圾收集器执行并行阶段时的线程数。
    -XX:ConcGCThreads=n 取决于不同平台 并发垃圾收集器运行时的线程数。
    -XX:G1ReservePercent=n 10 设置堆内存的预留占比,以减少晋升失败
    -XX:G1HeapRegionSize=n 1~32M G1中每个区域的大小。
  • 理解G1的GC日志

  • 有关GC日志,可以参考之前的这篇文章,针对G1垃圾收集器的GC日志细节,可以在这里找到答案。

  • 总结

  • 以上,则是有关G1垃圾收集器的基本原理和使用,若生产中的一些GC收集器始终无法达到预期,可以尝试使用G1。

  • 参考文献

  • G1垃圾收集指南

好人,一生平安。