G1 GC

G1GC基本概念

G1 GC_第1张图片

  • G1 GC可以看做是CMS GC的重大升级改造
  • G1 GC的全称是Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理他。
  • G1 GC最主要的设计目标是:将STW停顿的时间和分布,变成可预期且可配置的。(默认200ms)
    1. 垃圾回收过程中,一般垃圾的量在单位时间内都是固定的。
    2. 那么一下子把所有垃圾都处理完,就会导致了GC暂停的时间可能特别长。
    3. 如果每次处理一点,就会导致虽然暂停的时间比较短,但是整体垃圾回收的成本就会相对比较高。
    4. 为了平衡中间这个值,就产生了G1 GC的垃圾回收的算法。
    5. 设置了预期值之后,G1GC就会在自己执行了多次GC以后,调整自己GC策略:执行的频率、每次清理的垃圾数量等,慢慢地尽量控制在预期值以内。
    6. 这样的话就可以做到让GC暂停的时间跟整体的GC效率,比如吞吐量和延迟中间找到一个平衡点,这就是G1GC设计的一个出发点。
    7. 为了做到这件事情,G1GC打破了原先的串行并行CMS里堆内存整体的进行分区的年轻代(Eden、Servivor)、老年代(Old)的分代模式。
  • G1 GC 堆不在分成年轻代和老年代,而是划分为多个(默认是2048个)小块:Region。

每个小块,可能一会被定义成Eden区,一会被指定为Survivor区或者Old区。

这样内存上的管理:

  1. 颗粒度比较小
  2. 具体它用来做什么,都可以在运行期灵活地被G1GC所控制。
  • 启动参数:-XX:+UserG1GC   -XX:MaxGCPauseMillis=50

G1 GC_第2张图片

  • 这样划分以后,使得G1不必每次都区收集整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次GC的回收集(collection

set)。

  • 每次GC暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存

块。

  • G1的另一个创新是,在并发阶段估算每个小堆块存活对象的总数。

依据这些垃圾的数量,就可以计算出那些小块现在垃圾比较多,就可以优先去处理垃圾比较多的。进而提高整个垃圾回收的效率。

  • 构建回收集的原则是:垃圾最多的小块会被优先收集。这也是G1名称的由

来。

G1 GC--配置参数

  • -XX:+UseG1GC:启用 G1 GC;
  • -XXG1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%
    • 最开始只是用非常少量的这种小的块来作为整个Young区。
  • -XXG1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%
  • -XXG1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了;
  • -XXConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长;
  • -XX+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap 45%高水位)。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收;
    • 为何重要:因为如果老年代只使用了比较少的时候就做回收,这时要回收的整个对象比较少,垃圾也相对比较少,就会执行的特别快。如果这个比例特别大,整个老年代现在使用的量特别大,这时做垃圾回收时间就会特别长。
  • -XXG1HeapWastePercentG1停止回收的最小内存大小,默认是堆大小的 5%低水位)。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间;
  • -XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。
  • -XX:+G1PrintRegionLivenessInfo:这个参数需要和
  • -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个Region 里的对象存活信息。
  • -XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。
  • -XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。如果启用
  • -XX:G1SummaryRSetStatsPeriod 参数,就会阶段性地打印 Rsets 信息。
  • -XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。
  • -XX+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。
  • -XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用。
  • -XXMaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。

G1GC的处理步骤 1

  1. 年轻代模式转移暂停(Evacuation Pause)

G1GC会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂停时间,在应用程序刚启动时,G1还没有采集到什么足够的信息,这时候就处于初始的fully-young模式,当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存货区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。

拷贝的过程成为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。

  1. 并发标记(Concurrent Marking)

同时我们可以看到,G1GC的很多概念建立在CMS的基础上,所以下面的内容需要对CMS有一定的理解。

G1并发标记的过程与CMS基本上是一样的。G1的并发标记通过Snapshot-At-The-Beginning(起始快照)的方式,在标记阶段开始时记下所有的存活对象。即时在标记的同时又有一些变成了垃圾。通过对象的存活信息,可以构建出每个小堆块的存活状态,以便回收收集能高效地进行选择。

这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。

有两种情况是可以完全并发执行的:

  1. 如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾;
  2. 在STW转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。

当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是45%,但也可以通过JVM参数InitiatingHeapOccupancyPercent来设置。和CMS一样,G1的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。

G1GC的处理步骤 2

阶段1:Initial Mark(初始标记)

此阶段标记所有从GC根对象直接可达的对象。

阶段2:Root Region Scan(Root区扫描)

此阶段标记所有从”根区域“可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。

阶段3:Concurrent Mark(并发标记)

此阶段和CMS的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。

阶段4:Remark(再次标记)

和CSM类似,这是一次STW停顿(因为不是并发的阶段),以完成标记过程。G1收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。

阶段5:Cleanup(清理)

最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC的效率,维护并发标记的内部状态。所有不包括存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的STW暂停。

G1GC的处理步骤 3

转移暂停:混合模式(Evacuation Pause(mixed))

并发标记完成之后,G1将完成一次混合收集(mixed collection),就是不只清理年轻代,还将一部分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有了必要启动混合模式。

因此,在并发标记与混合转移暂停之间,很可能会存在多次young模式的转移暂停。

具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时性能指标,存活性,以及在并发标记期间收集的GC效率等数据,外加一些可配置的JVM选项。混合收集的过程,很大程度上和前面的fully-young gc是一样的。

G1GC的注意事项

特别需要注意的是,某些情况下G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。会触发这种情况的条件如下:

  1. 并发模式失败

G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。

解决办法:增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

  1. 晋级失败

没有足够的内存供存活对象或晋级对象使用,由此触发了Full GC(to-space exhausted/to-space overflow)。

补充:当G1GC做垃圾回收的时候,有很多对象需要从新生代晋升到老年代,晋升的时候发现没有足够的内存供这次晋升使用,这时候也会触发Full GC,然后进行GC退化。

解决办法:

  1. 增加-XX:G1ReservePercent选项的值(并相应增加总的堆大小)增加预留内存量。

补充:G1GC内部为了更有效的做GC,其实默认是保留一部分的内存的,用来做对象的复制。

  1. 通过减少-XX:InitiatingHeapOccupancyPercent (默认45%)提前启动标记周期,进入垃圾回收。这样空余的内存相对就多了。
  2. 也可以通过增加-XX:ConcGCThreads选项的值来增加并行标记线程的数目。
  1. 巨型对象分配失败

当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。

解决办法:增加内存或者增大-XX:G1HeapRegionSize

各个GC对比

G1 GC_第3张图片

GC如何选择

选择正确的 GC 算法,唯一可行的方式就是去尝试,一般性的指导原则:

  1. 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;
  2.  如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC;
  3.  如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。

对于内存大小的考量:

  1. 一般 4G 以上,算是比较大,用 G1 的性价比较高。
  2. 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1 GC。

最后讨论一个很多开发者经常忽视的问题,也是面试大厂常问的问题:JDK8 的默认 GC 是什么?

JDK9,JDK10,JDK11…等等默认的 GC 是什么?

你可能感兴趣的:(JVM,java进阶,java)