上一篇专栏简单介绍了一下GC,使我们对Oracle JDK的GC有一定的了解,有Serial GC这种古老简单的单线程计算模式,也有CMS并行计算收集机制,还有新型调优思路G1 GC。
之后我们又介绍了单线程集中式的GC流程。简单来说,就是程序运行的过程中,对当前周期使用到堆内部的对象实训,进行标记,其他进行清除不断循坏,并且在这个过程中进行一定的内存管理。最后成为老年代。
而老年代的GC不论是诞生的过程和在这之后GC,都与使用的具体GC使用策略有关。
那么,既然我们已经知道了这个流程了,下面就来和大家说一下GC的调优思路。
现在要明确一件事:调优的目标,究竟是为什么?
首先,从性能角度来分析,通常关注三个方面:
大多数情况下调优会侧重于其中1-2个角度去调优,很少有三个方面全部考虑的。
基本的调优思路可以总结为:
GC调优是JVM调优的一个基础,有很多JVM调优的需求,最后都会落实在GC上或者与其相关。我们不仅仅要知道这些理论层面的知识,还要有项目中产生的直觉,可以从网上找到一些联系的机会,在专栏中会更侧重关于思路。
这次,围绕G1 GC来讲,接下来主要分析下面两点:
首先,来了解一下G1 GC的内部结构和主要机制。
G1内部同样存在年代的概念,但是内部结构编程了region:
每一个region的大小是一致的,但是数值在1-32MB之间的一个2的幂指数,JVM会尽量划分2048个同等大小的region,当然这个数字也会手动调整。
在G1实现中,年代是一个逻辑概念,具体体现在,一部分region是作为Eden,一部分Survivor,除了意料中的老年代,G1会将一半多的在堆内的对象都归类为Humongous对象,并且放置在region中。从逻辑上来说复制超大规模的对象是很消耗性能,必然不是新生代的GC算法,所以也算是老年代一部分。
那么,region的设计有什么副作用吗?
例如,其大小均等的划分看似很厉害,但是实际上,大对象大于一个region放不进去,小对象放进去浪费,这就是JVM设计的问题所在,尽管解决问题很简单,那就是直接设置比较大的region大小,参数如下:
-XX:G1HeapRegionSize=M
从GC算法的角度来说,G1所谓的符合算法们可以简单理解为:
从习惯上来讲,新生代GC被叫做Minor GC,老年代GC被叫做Major GC,区别于整体性的Full GC。 但是现代GC中,这种概念已经不再准确,对于G1来说:
–XX:G1MixedGCLiveThresholdPercent –XX:G1OldCSetRegionThresholdPercent
从G1内部运行的角度来说,下面是运行图但是当逃逸失败的等情况,会触发Full GC。
然后说一下Remembered Set操作究竟是什么
关键在于记录与维护region之间对象的引用关系,保证在Eden/Survivor -> to区域的对象移动复制操作,跨区引用也依然有效。
G1的很多开销源自于Remembered Set,由于它的迁移对象,会导致占用堆自身内存20%的情况或者更高,影响了复制的速度,进而影响暂停时间
接下来,我介绍下大家可能还不了解的G1行为变化,它们在一定程度上解决了专栏其他讲中提到的部分困扰,如类型卸载不及时的问题。
-XX:+UseStringDeduplication
注意,这种排重虽然减少内存占用,但是这种并发操作会占用一些CPU资源,也会导致Young GC稍微变慢。
旧版本G1之后再Full GC的时候才会进行类型卸载,可以用以下参数查看类型卸载:
-XX:+TraceClassUnloading
新版G1做了一定优化,8u40只有,默认开启以下选项,只在并发标记只后才卸载:
-XX:+ClassUnloadingWithConcurrentMark
-XX:InitiatingHeapOccupancyPercent
新版G1,会将该参数设置为初始值,在运行的时候进行采样,动态调整并发标记启动时机,对应参数如下:
-XX:+G1UseAdaptiveIHOP
尽量升级到高版本的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内部结构和机制,很多结论就一目了然了,例如:
-XX:G1NewSizePercent
降低其最大值同样对降低Young GC延迟有帮助。
-XX:G1MaxNewSizePercent
如果我们直接为G1设置较小的延迟目标值,也会起到减小新生代的效果,虽然会影响吞吐量。
XX:G1MixedGCCountTarget