谈谈你的GC调优思路?

上一篇专栏简单的介绍了一下GC,让我们对于oracleJDK的gc有了一定的了解,有Serial GC这种古老简单的单线程计算模式,也有CMS并行计算收集机制,还有新型调优思路G1 GC。

之后我们又介绍了单线程集中式的gc流程。简单来说,就是程序运行的过程中,对当前周期使用到的堆内部的对象实例,进行标记,其他进行清除,不断循环,并且在这个过程中进行一定的内存整理。最后成为老年代。

而老年代的gc不论是诞生的过程和在这之后的gc,都与使用的具体gc使用策略有关。

那么,既然我们已经知道了这个流程了,下面就来和大家说一下gc的调优思路。

概述

我们要明确一件事情:调优的目标,究竟是为什么?

首先,从性能角度去分析,通常关注三个方面:

  1. 内存占用(foorprint)
  2. 延时(latency)
  3. 吞吐量(throughput)

大多数情况家会侧重1-2个角度去调优,很少有三个方面全部考虑的。

基本的调优思路可以总结为以下几点:

  • 理解针对应用的需求和问题,确定调优目标。假如我们开发了一个产品,经常会出现性能抖动、延迟的问题,那么再评估完用户需求之后,就需要对于gc的暂停,有一个量化的控制,同时保证一定的吞吐量。

  • 掌握jvm和gc的状态,定位具体的问题,确定自己真的需要gc调优。方法有很多。比如使用jstat查看gc状态,开启gc日志,利用操作系统提供的诊断工具等等。

  • 选择的gc类型是否符合应用场景。比如Minor GC会异常停顿,cms和G1更适合低延迟。

  • 分析得出要调整的参数或者软硬件配置

分析思路

GC调优是jvm调优的一个基础,有很多jvm调优的需求,最后都会落实在gc上或者与其相关。我们不仅仅要知道这些理论层面的知识,还要有项目中产生的直觉,可以从网上找到一些联系的机会,在专栏中会更侧重关于思路。

这次,围绕G1 GC来讲,接下来,会主要分析下面两点:

  1. 由于G1 GC是现在时代的首选,所以在手机gc日志方面有了很大改善。会说说具体内部结构和主要机制。

  2. 从调优实践的角度,理解通用的调优思路。

扩展

首先,来了解一下G1 GC的内部结构和主要机制。

G1内部同样存在年代的概念,但是内部结构变成了region:

在这里插入图片描述
每一个region的大小是一致的,但是数值在1-32MB之间的一个2的幂指数,jvm会尽量划分2048个同等大小的region,当然这个数字也会手动调整。

在G1实现中,年代是一个逻辑概念,具体体现在,一部分region是作为Eden,一个部分作为Survivor,除了意料之中的老年代,G1会将一半多的在堆内的对象都归类为Humongous对象,并且放置在对应的region中。从逻辑上来说,复制超大规模的对象是很消耗性能的,必然不是新生代的gc算法,所以,也算是老年代的一部分。

那么,region的设计有什么副作用吗?

例如,其大小均等的划分看似很nb,但是实际上,大对象大于一个region放不进去,小对象放进去浪费,这就是jvm设计的问题所在,尽管解决问题很简单,那就是直接设置比较大的region大小,参数如下:

-XX:G1HeapRegionSize=<N, 例如16>M

从GC算法的角度来说,G1所谓的复合算法们可以简单理解为:

  • 在新生代,G1采用的人仍然是并行的复制算法,所以同样会发生Stop-The-World的暂停

  • 在老年代,大部分情况下都是并发标记,而整理是老年代与新生代同时的,所以整理是增量进行的。

从习惯上来讲,新生代GC被叫做Minor GC,老年代GC被叫做Major GC,区别于整体性的Full GC。但是现代GC中,这种概念已经不再准确,对于G1来说:

  • Minor GC仍然存在,但是会涉及到Remembered Set等相关处理。
  • 老年代 GC ,则是依靠Mixed GC。意思是,并发标记结束后,Mixed GC会进行clean操作,清理Eden Survivor,还会清理部分old区域。通过设置阈值来设定最多包含在一次Mixed GC的region比例。

–XX:G1MixedGCLiveThresholdPercent –XX:G1OldCSetRegionThresholdPercent

从G1内部运行的角度来说,下面是运行图,但是当逃逸失败等情况,会触发Full GC。
在这里插入图片描述

然后说一下Remembered Set操作究竟是什么

其关键在于记录与维护region之间对象的引用关系,保证在Eden/Survivor -> to区域的对象移动复制操作,跨区引用也依然有效。

在这里插入图片描述

G1的很多开销源自于Remembered Set,由于它的迁移对象,会导致占用堆自身内存20%的情况或者更高,影响了复制的速度,进而影响暂停时间


接下来,我介绍下大家可能还不了解的G1行为变化,它们在一定程度上解决了专栏其他讲中提到的部分困扰,如类型卸载不及时的问题。

  • 在旧版G1中,Humongous通常都是在并发标记之后才会开始进行回收,这就会造成一定内存问题,但是在新版G1中,因为时刻会记录老年代region的引用情况,那么在未被引用的情况下,唯一一种不被回收的情况,就是新生代调用,而新生代的相关信息是在Young GC就已经知道的,所以就可以在并发的同时进行回收

  • 在jdk8中,8u20之后的版本,在gc阶段,G1会把新创建的字符串放进队列。并发之后会对内部数据排重,也就是引用同一数组。可以采用以下语句激活:

-XX:+UseStringDeduplication

这种排重,虽然减少内存占用了,但是又会消耗cpu资源,所以有利有弊。

  • 类型卸载是在java方面的一个难题,我们从ClassLoader的专栏中知道,一个类只有当其自定义的classLoader被回收之后才会卸载,但是还是会出现问题。

旧版G1之后在Full GC的时候才会进行类型卸载,可以用以下参数查看类型卸载:

-XX:+TraceClassUnloading

新版G1做了一定优化,8u40之后,默认开启yixia以下选项,只在并发标记之后才卸载

-XX:+ClassUnloadingWithConcurrentMark

  • 我们知道老年代GC要在等待并发标记之后进行对象回收。这就意味着,当并发标记不足的时候,堆就会满,而gc还没有完成,就会触发Full GC,所以触发并发标记的时机很重要。
    早期的G1调优可以通过以下设置进行调整:

-XX:InitiatingHeapOccupancyPercent

而新版G1,会将该参数设置为初始值,在运行的时候进行采样,动态调整并发标记启动时机,对应参数如下:

-XX:+G1UseAdaptiveIHOP

  • 在现有的资料中,大多指出G1的Full GC是最差劲的单线程串行GC。其实,如果采用的是最新的JDK,你会发现Full GC也是并行进行的了,在通用场景中的表现还优于Parallel GC的Full GC实现。

调优建议

第一

尽量升级到高版本jdk ,如上所述,新版本jdk减少很多问题,优化很多方案

第二

掌握GC调优信息收集途径。掌握尽量全面、详细、准确的信息,是各种调优的基础。
良好的使用日志可以事半功倍。

有两个常用的选项:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

还有一些非常有用的日志选项,很多特定问题的诊断都是要依赖这些选项

-XX:+PrintAdaptiveSizePolicy // 打印G1 Ergonomics相关信息

我们非常清楚,gc内部一些行为是适应性触发,利用用PrintAdaptiveSizePolicy,就可以知道为啥jvm做出了一些我们不希望发生的事情。例如,G1调优的基本建议就是避免进行大量的Humongous对象分配,如果Ergonomics信息说明发生了这一点,那么就可以考虑要么增大堆的大小,要么直接将region大小提高。

如果是怀疑出现引用清理不及时的情况,则可以打开下面选项,掌握到底是哪里出现了堆积

-XX:+PrintReferenceGC

另外,建议开启选项下面的选项进行并行引用处理。

-XX:+ParallelRefProcEnabled

需要注意的一点是,JDK 9中JVM和GC日志机构进行了重构,其实我前面提到的PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会导致JVM无法启动。可以使用下面的命令查询新的配置参数。

java -Xlog:help

最后,看一些通用实践,理解了gc内部结构和机制,很多结论就一目了然了,例如:

  • 如果发现Young GC非常耗时,这很可能就是因为新生代太大了,我们可以考虑减小新生代的最小比例。

-XX:G1NewSizePercent
降低其最大值同样对降低Young GC延迟有帮助。
-XX:G1MaxNewSizePercent
如果我们直接为G1设置较小的延迟目标值,也会起到减小新生代的效果,虽然会影响吞吐量。

  • 如果是Mixed GC延迟较长,我们应该怎么做呢?
    部分Old region会被包含进Mixed GC,减少一次处理的region个数,就是个直接的选择之一。
    上面已经介绍了G1 OldCSetRegionThresholdPercent控制其最大值,还可以利用下面参数提高Mixed GC的个数,当前默认值是8,Mixed GC数量增多,意味着每次被包含 的region减少。

-XX:G1MixedGCCountTarget