返回文章列表
JVM虚拟机
JVMGC调优FullGC

08GC调优

GC 调优指的是对垃圾回收(Garbage Collection)进行调优。GC 调优的主要目标是避免由垃圾回收引起程序性能下降。

GC 调优的核心分成三部分:

1、通用 Jvm 参数的设置。

2、特定垃圾回收器的 Jvm 参数的设置。

3、解决由频繁的 FULLGC 引起的程序性能问题。

GC 调优没有唯一的标准答案,如何调优与硬件、程序本身、使用情况均有关系,重点学习调优的工具和方法。

GC 调优的步骤总共分为四个步骤:

  1. 发现:通过监控工具尽可能早地发现 GC 时间过长、频率过高的现象。
  2. 诊断:通过分析工具,诊断问题的产生原因。
  3. 修复:修复问题。
  4. 验证:测试验证。

GC 调优的四个步骤

1.GC调优的核心指标

吞吐量、延迟、内存使用量是 GC 调优的三大核心指标,与 GC 算法的评判标准类似。

1.1 吞吐量

吞吐量(Throughput)分为业务吞吐量和垃圾回收吞吐量。

业务吞吐量指的是在一段时间内,程序需要完成的业务数量。比如企业中对于吞吐量的要求可能会是这样的:

  • 支持用户每天生成 10000 笔订单
  • 在晚上 8 点到 10 点,支持用户查询 50000 条商品信息

保证高吞吐量的常规手段有两条:

1、优化业务执行性能,减少单次业务的执行时间

2、优化垃圾回收吞吐量

但是我们本文是 JVM 相关内容,关注于垃圾吞吐量。

垃圾回收吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即:

吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)

吞吐量数值越高,垃圾回收的效率就越高,允许更多的 CPU 时间去处理用户的业务,相应的业务吞吐量也就越高。比如:虚拟机总共运行了 100 秒,其中 GC 花掉 1 秒,那么吞吐量就是 99%。

垃圾回收吞吐量示意:执行用户代码 99 秒 + GC STW 1 秒

1.2 延迟

延迟指的是从用户发起一个请求到收到响应这其中经历的时间。比如企业中对于延迟的要求可能会是这样的:

  • 所有的请求必须在 5 秒内返回给用户结果

延迟 = GC 延迟 + 业务执行时间,所以如果 GC 时间过长,会影响到用户的使用。比如:用户请求收到并执行 2 秒、GC STW 3 秒、用户请求执行并返回 1 秒,总处理时间就超过了 5 秒。

1.3 内存使用量

内存使用量指的是 Java 应用占用系统内存的最大值,一般通过 Jvm 参数调整,在满足上述两个指标的前提下,这个值越小越好。

2.GC调优常用工具

2.1 jstat 工具

Jstat 工具是 JDK 自带的一款监控工具,可以提供各种垃圾回收、类加载、编译信息等不同的数据。使用方法为:

jstat -gc 进程ID 每次统计的间隔(毫秒) 统计次数

输出列含义:

  • C 代表 Capacity 容量,U 代表 Used 使用量
  • S – 幸存者区,E – 伊甸园区,O – 老年代,M – 元空间
  • YGC、YGT:年轻代 GC 次数和 GC 耗时(单位:秒)
  • FGC、FGCT:Full GC 次数和 Full GC 耗时
  • GCT:GC 总耗时

优点: 操作简单,无额外的软件安装。

缺点: 无法精确到 GC 产生的时间,只能用于判断 GC 是否存在问题。

2.2 Visualvm 插件

VisualVm 中提供了一款 Visual GC 插件,实时监控 Java 进程的堆内存结构、堆内存变化趋势以及垃圾回收时间的变化趋势。同时还可以监控对象晋升的直方图。

优点: 适合开发使用,能直观地看到堆内存和 GC 的变化趋势。

缺点: 对程序运行性能有一定影响,生产环境程序员一般没有权限进行操作。

2.3 Prometheus + Grafana

Prometheus + Grafana 是企业中运维常用的监控方案,其中 Prometheus 用来采集系统或者应用的相关数据,同时具备告警功能。Grafana 可以将 Prometheus 采集到的数据以可视化的方式进行展示。Java 程序员要学会如何读懂 Grafana 展示的 Java 虚拟机相关的参数。

优点: 支持系统级别和应用级别的监控,比如 linux 操作系统、Redis、MySQL、Java 进程;支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理。

缺点: 环境搭建较为复杂,一般由运维人员完成。

2.4 GC 日志与可视化工具

通过 GC 日志,可以更好的看到垃圾回收细节上的数据,同时也可以根据每款垃圾回收器的不同特点更好地发现存在的问题。

使用方法:

  • JDK 8 及以下:-XX:+PrintGCDetails -Xloggc:文件名
  • JDK 9+:-Xlog:gc*:file=文件名

GCViewer 是一个将 GC 日志转换成可视化图表的小工具,github 地址:https://github.com/chewiebug/GCViewer。使用方法:`java -jar gcviewer_1.3.4.jar 日志文件.log`,右下角是基础信息,左边是内存趋势图。

GCeasy 是业界首款使用 AI 机器学习技术在线进行 GC 分析和诊断的工具。定位内存泄漏、GC 延迟高的问题,提供 JVM 参数优化建议,支持在线的可视化工具图表展示。官方网站:https://gceasy.io/。使用方法:

1、选择文件,找到 GC 日志并上传。

2、点击 Analyze 分析就可以看到报告,每个账号每个月能免费上传 5 个 GC 日志。报告中包含建议、内存情况、GC 关键性指标、GC 的趋势图、引发 GC 的原因等。

2.5 常见的 GC 模式

根据内存的趋势图,我们可以将 GC 的情况分成几种模式:

一、正常情况:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,存留的对象较少。

二、缓存对象过多:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,处于比较高的位置。

问题产生原因:程序中保存了大量的缓存对象,导致 GC 之后无法释放,可以使用 MAT 或者 HeapHero 等工具进行分析内存占用的原因。

三、内存泄漏:呈现锯齿状,每次垃圾回收之后下降到的内存位置越来越高,最后由于垃圾回收无法释放空间导致对象无法分配产生 OutOfMemory 的错误。

问题产生原因:程序中保存了大量的内存泄漏对象,导致 GC 之后无法释放,可以使用 MAT 或者 HeapHero 等工具进行分析是哪些对象产生了内存泄漏。

四、持续的 FullGC:在某个时间点产生多次 Full GC,CPU 使用率同时飙高,用户请求基本无法处理。一段时间之后恢复正常。

问题产生原因:在该时间范围请求量激增,程序开始生成更多对象,同时垃圾收集无法跟上对象创建速率,导致持续地在进行 FULL GC。

五、元空间不足导致的 FULLGC:堆内存的大小并不是特别大,但是持续发生 FULLGC。

问题产生原因:元空间大小不足,导致持续 FULLGC 回收元空间的数据。元空间并不是满了才触发 FULLGC,而是 JVM 自动会计算一个阈值,如下图中元空间并没有满,但是频繁产生了 FULLGC。

3.解决 GC 问题的手段

解决 GC 问题的手段中,前三种是比较推荐的手段,第四种仅在前三种无法解决时选用:

  • 优化基础 JVM 参数:基础 JVM 参数的设置不当,会导致频繁 FULLGC 的产生。
  • 减少对象产生:大多数场景下的 FULLGC 是由于对象产生速度过快导致的,减少对象产生可以有效的缓解 FULLGC 的发生。
  • 更换垃圾回收器:选择适合当前业务场景的垃圾回收器,减少延迟、提高吞吐量。
  • 优化垃圾回收器参数:优化垃圾回收器的参数,能在一定程度上提升 GC 效率。

3.1 优化基础 JVM 参数

参数 1:-Xmx 和 -Xms

-Xmx 参数设置的是最大堆内存,但由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统、其它软件占用的内存排除掉。

案例:服务器内存 4G,操作系统+元空间最大值+其它软件占用 1.5G,-Xmx 可以设置为 2g。最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值。

堆内存与元空间、操作系统等内存分配关系

-Xms 用来设置初始堆大小,建议将 -Xms 设置的和 -Xmx 一样大,有以下几点好处:

  • 运行时性能更好,堆的扩容是需要向操作系统申请内存的,这样会导致程序性能短期下降。
  • 可用性问题,如果在扩容时其他程序正在使用大量内存,很容易因为操作系统内存不足分配失败。
  • 启动速度更快,Oracle 官方文档的原话:如果初始堆太小,Java 应用程序启动会变得很慢,因为 JVM 被迫频繁执行垃圾收集,直到堆增长到更合理的大小。为了获得最佳启动性能,请将初始堆大小设置为与最大堆大小相同。

参数 2:-XX:MaxMetaspaceSize 和 -XX:MetaspaceSize

-XX:MaxMetaspaceSize=值 参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为 256m。

-XX:MetaspaceSize=值 参数指的是到达这个值之后会触发 FULLGC(网上很多文章的初始元空间大小是错误的),后续什么时候再触发 JVM 会自行计算。如果设置为和 MaxMetaspaceSize 一样大,就不会 FULLGC,但是对象也无法回收。

参数 3:-Xss 虚拟机栈大小

如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。比如 Linux x86 64 位:1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为 256k – 1m 之间。

使用:-Xss256k

参数 4:不建议手动设置的参数

由于 JVM 底层设计极为复杂,一个参数的调整也许让某个接口得益,但同样有可能影响其他更多接口。

-Xmn 年轻代的大小,默认值为整个堆的 1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。G1 垃圾回收器尽量不要设置该值,G1 会动态调整年轻代的大小。

-XX:SurvivorRatio 伊甸园区和幸存者区的大小比例,默认值为 8。

-XX:MaxTenuringThreshold 最大晋升阈值,年龄大于此值之后,会进入老年代。另外 JVM 有动态年龄判断机制:将年龄从小到大的对象占据的空间加起来,如果大于 survivor 区域的 50%,然后把等于或大于该年龄的对象,放入到老年代。

比如下图中,年龄1+年龄2+年龄3 = 55m 已经超过了 S 区的 50%,所以会将年龄 3 及以上的对象全部放入老年代。

其他参数:

  • -XX:+DisableExplicitGC:禁止在代码中使用 System.gc(),System.gc() 可能会引起 FULLGC,在代码中尽量不要使用。
  • -XX:+HeapDumpOnOutOfMemoryError:发生 OutOfMemoryError 错误时,自动生成 hprof 内存快照文件。
  • -XX:HeapDumpPath=<path>:指定 hprof 文件的输出路径。

打印 GC 日志:

  • JDK8 及之前:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径
  • JDK9 及之后:-Xlog:gc*:file=文件路径

JVM 参数模板:

-Xms1g
-Xmx1g
-Xss256k
-XX:MaxMetaspaceSize=512m
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/my-service.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:文件路径

注意:JDK9 及之后 gc 日志输出修改为 -Xlog:gc*:file=文件名。堆内存大小和栈内存大小根据实际情况灵活调整。

3.2 垃圾回收器的选择

思路:

  1. 编写 Jmeter 脚本对程序进行压测,同时添加 RT 响应时间、每秒钟的事务数等指标进行监控。
  2. 选择不同的垃圾回收器进行测试,并发量分别设置 50、100、200,观察数据的变化情况。

垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为年轻代和老年代,除了 G1 之外其他垃圾回收器必须成对组合进行使用。

常用组合:

  • JDK8 默认组合:PS + PO(Parallel Scavenge + Parallel Old)
  • JDK8 下 ParNew + CMS 组合:-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
  • JDK8 使用 G1:-XX:+UseG1GC
  • JDK11 默认使用 G1

测试基础 JVM 测试参数:

-Xms8g -Xmx8g -Xss256k -XX:MaxMetaspaceSize=512m  -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/test.hprof  -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

测试结果:

垃圾回收器 参数 50并发(最大响应时间) 100并发(最大响应时间) 200并发(最大响应时间)
PS+PO 默认 260ms 474ms 930ms
CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 157ms 未测试 833ms
G1 JDK11默认 未测试 未测试 248ms

由此可见使用了 JDK11 之后使用 G1 垃圾回收器,性能优化结果还是非常明显的。

3.3 优化垃圾回收器的参数

这部分优化效果未必出色,仅当前边的一些手段无效时才考虑。

一个优化的案例:CMS 的并发模式失败(concurrent mode failure)现象。由于 CMS 的垃圾清理线程和用户线程是并行进行的,如果在并发清理的过程中老年代的空间不足以容纳放入老年代的对象,会产生并发模式失败。

解决方案:

  1. 减少对象的产生以及对象的晋升。
  2. 增加堆内存大小。
  3. 优化垃圾回收器的参数,比如 -XX:CMSInitiatingOccupancyFraction=值,当老年代大小到达该阈值时,会自动进行 CMS 垃圾回收,通过控制这个参数提前进行老年代的垃圾回收,减少其大小。

JDK8 中默认这个参数值为 -1,根据其他几个参数计算出阈值:

((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)

在我本机计算之后的结果是 92。该参数设置完是不会生效的,必须开启 -XX:+UseCMSInitiatingOccupancyOnly 参数。

调整前和调整之后的效果对比,很明显 FULLGC 产生的次数下降了。