上一篇专栏简单的介绍了一下GC,让我们对于oracleJDK的gc有了一定的了解,有Serial GC这种古老简单的单线程计算模式,也有CMS并行计算收集机制,还有新型调优思路G1 GC。
之后我们又介绍了单线程集中式的gc流程。简单来说,就是程序运行的过程中,对当前周期使用到的堆内部的对象实例,进行标记,其他进行清除,不断循环,并且在这个过程中进行一定的内存整理。最后成为老年代。
而老年代的gc不论是诞生的过程和在这之后的gc,都与使用的具体gc使用策略有关。
那么,既然我们已经知道了这个流程了,下面就来和大家说一下gc的调优思路。
我们要明确一件事情:调优的目标,究竟是为什么?
首先,从性能角度去分析,通常关注三个方面:
大多数情况家会侧重1-2个角度去调优,很少有三个方面全部考虑的。
基本的调优思路可以总结为以下几点:
理解针对应用的需求和问题,确定调优目标。假如我们开发了一个产品,经常会出现性能抖动、延迟的问题,那么再评估完用户需求之后,就需要对于gc的暂停,有一个量化的控制,同时保证一定的吞吐量。
掌握jvm和gc的状态,定位具体的问题,确定自己真的需要gc调优。方法有很多。比如使用jstat查看gc状态,开启gc日志,利用操作系统提供的诊断工具等等。
选择的gc类型是否符合应用场景。比如Minor GC会异常停顿,cms和G1更适合低延迟。
分析得出要调整的参数或者软硬件配置
GC调优是jvm调优的一个基础,有很多jvm调优的需求,最后都会落实在gc上或者与其相关。我们不仅仅要知道这些理论层面的知识,还要有项目中产生的直觉,可以从网上找到一些联系的机会,在专栏中会更侧重关于思路。
这次,围绕G1 GC来讲,接下来,会主要分析下面两点:
由于G1 GC是现在时代的首选,所以在手机gc日志方面有了很大改善。会说说具体内部结构和主要机制。
从调优实践的角度,理解通用的调优思路。
首先,来了解一下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=
M
从GC算法的角度来说,G1所谓的复合算法们可以简单理解为:
在新生代,G1采用的人仍然是并行的复制算法,所以同样会发生Stop-The-World的暂停
在老年代,大部分情况下都是并发标记,而整理是老年代与新生代同时的,所以整理是增量进行的。
从习惯上来讲,新生代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行为变化,它们在一定程度上解决了专栏其他讲中提到的部分困扰,如类型卸载不及时的问题。
在旧版G1中,Humongous通常都是在并发标记之后才会开始进行回收,这就会造成一定内存问题,但是在新版G1中,因为时刻会记录老年代region的引用情况,那么在未被引用的情况下,唯一一种不被回收的情况,就是新生代调用,而新生代的相关信息是在Young GC就已经知道的,所以就可以在并发的同时进行回收。
在jdk8中,8u20之后的版本,在gc阶段,G1会把新创建的字符串放进队列。并发之后会对内部数据排重,也就是引用同一数组。可以采用以下语句激活:
-XX:+UseStringDeduplication
这种排重,虽然减少内存占用了,但是又会消耗cpu资源,所以有利有弊。
旧版G1之后在Full GC的时候才会进行类型卸载,可以用以下参数查看类型卸载:
-XX:+TraceClassUnloading
新版G1做了一定优化,8u40之后,默认开启yixia以下选项,只在并发标记之后才卸载
-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