GC调优

GC调优

GC的各项指标,是衡量Java进程内存使用是否健康的重要标尺。

GC优化步骤

GC优化一般步骤可以概括为:确定目标、优化参数、验收结果。首先,明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如:

  • 高可用,可用性达到几个9
  • 低延迟,请求必须多少毫秒内完成响应。
  • 高吞吐,每秒完成多少次事务。

明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。一般的业务场景会偏重于关注高可用与低延迟,如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前GC情况对服务的影响,也能评估出GC优化后对响应时间的收益,这两点对于低延迟服务很重要。

业务指标投射到GC的核心指标,就是GC Pause(包括MinorGCMajorGC)的频率和次数,以及每次回收的内存详情。假设单位时间T内发生一次持续25msGC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:

GC 对业务请求影响的示意图

那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25msGC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生NGC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。

GC Pause的频率和次数可以通过jstat工具直接得到,内存回收详情则需要分析GC日志。通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。

常见的调优策略

由于垃圾回收器种类繁多,针对不同的应用,调优策略也有所区别,因此下面介绍几种通用的的GC调优策略。

  • 选择合适的GC回收器。根据应用对延迟、吞吐的要求,结合各垃圾回收器的特点,合理选用。推荐使用G1替换CMS垃圾回收器,G1的性能是在逐步优化的,在8GB内存及以下的机器上,其各方面的表现也在赶上甚至有超越之势。G1调参较方便,而CMS垃圾回收器参数太过复杂、容易造成空间碎片化、对CPU消耗较高等弊端,也使其目前处于废弃状态。Java 11里新引入的ZGC垃圾回收器,基本可用做到全阶段并发标记和回收,值得期待。

  • 合理的堆内存大小设置。堆大小不要设置过大,建议不要超过系统内存的75%,避免出现系统内存耗尽。最大堆大小和初始化堆的大小保持一致,避免堆震荡。新生代的大小设置比较关键,我们调整GC的频率和耗时,很多时候就是在调整新生代的大小,包括新生代和老年代的占比、新生代中Eden区和Survivor区的比例等,这些比例的设置还需要考虑各代中对象的晋升年龄,整个过程需要考虑的东西还是比较多的。如果使用G1垃圾回收器,新生代大小这一块需要考虑的东西就少很多了,自适应的策略会决定每一次的回收集合(CSet。新生代的调整是GC调优的核心,非常依赖经验,但是一般来说,Young GC频率高,意味着新生代太小(或Eden区和Survivor配置不合理Young GC时间长,意味着新生代过大,这两个方向大体不差。

  • 降低Full GC的频率。如果出现了频繁的Full GC或者 老年代GC,很有可能是存在内存泄漏,导致对象被长期持有,通过dump内存快照进行分析,一般能较快地定位问题。除此之外,新生代和老年代的比例不合适,导致对象频频被直接分配到老年代,也有可能会造成Full GC,这个时候需要结合业务代码和内存快照综合分析。

  • 通过配置GC参数,可以帮助我们获取很多GC调优所需的关键信息,如配置 -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics -XX:+PrintTenuringDistribution,分别可以获取GC Pause分布、安全点耗时统计、对象晋升年龄分布的信息,加上-XX:+PrintFlagsFinal可以让我们了解最终生效的GC参数等。

Links