GC 调优

GC 调优

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

GC 优化步骤

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

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

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

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

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

那么有(50ms+25ms)/T 比例的请求会受 GC 影响,其中 GC 前的 50ms 内到达的请求都会增加 25ms,GC 期间的 25ms 内到达的请求,会增加 0-25ms 不等,如果时间 T 内发生 N 次 GC,受 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