每小时进行一次FullGC

本文分析了因JMX开启导致应用每小时自动执行FullGC的问题。通过代码排查和JDK源码分析,发现RMIGCDaemon线程周期性调用System.gc()。提供了三种解决方案,包括调整gcInterval参数、禁用RMIFullGC和禁用显式GC调用。

1. 问题描述

  在排查接口响应慢的过程中,通过prometheus监控系统看到JVM每隔一小时进行一次FullGC(非常准时),我们都知道FullGC是STW的,所以有必要进行深入排查。

2. 问题分析

  1. 每小时准时进行一次FullGC,我首先怀疑可能是应用本身有定时任务显式调用 System.gc() 进行主动FullGC,排查代码后没有发现主动调用System.gc(),查看gc.log后发现存在System.gc()调用,查看其他应用存在同样的问题,看来是个通用问题;
  2. jstack -l {pid} 查看应用进程的堆栈信息,发现有一个 RMI GC Daemon 和 RMI RenewClean 的 守护线程,应用是开启JMX的,所以会有这些进程;
    在这里插入图片描述
    在这里插入图片描述
  3. 通过进程堆栈信息,查看JDK源码(jdk1.8.0_242),发现 GC.java 中 Daemon 静态内部类有一个自旋进行 System.gc();DGCClient.java 中 sun.rmi.dgc.client.gcInterval 可以配置FullGC时间间隔默认3600秒。
    在这里插入图片描述
    在这里插入图片描述

结论:
  由于应用开启了JMX对接监控系统,开启JMX后JDK默认会每小时进行一次 System.gc();

3. 解决方法

解决方法1:添加 -Dsun.rmi.dgc.client.gcInterval 属性并配置合理的值,默认是3600000毫秒(1小时),可以适当调大;

解决方法2:禁用RMI FullGC,添加-Dsun.rmi.dgc.client.gcInterval属性并将值设置为 Long.MAX_VALUE即 9223372036854775807;

解决方法3:禁止显示调用System.gc(),添加JVM选项 -XX:+DisableExplicitGC,不建议添加此JVM选项禁用显式调用.System.gc(),影响范围过大,且某些三方组件依赖System.gc()。

一天发生 **11 次 Full GC** 是否有问题,取决于你的应用类型、堆内存大小、业务负载以及每次 Full GC 的持续时间和影响。但从一般生产环境的稳定性与性能角度来说,**一天 11 次 Full GC 是偏高的,通常意味着存在潜在问题**,需要深入排查。 --- ### ✅ 正常情况下的 Full GC 频率 - 理想情况下:Full GC 应该很少发生,比如几天一次甚至更少。 - 轻量级服务或中等负载应用:每月几次或每周一次是可接受的。 - 如果每天超过 **5~6 次 Full GC**,就需要引起注意。 - **11 次/天 ≈ 每 2 小时一次**,这在大多数场景下属于 **异常频繁**。 --- ### ❌ 可能的问题原因 | 原因 | 说明 | |------|------| | **老年代空间不足** | 对象过早晋升到老年代,或大对象直接进入老年代导致频繁回收。 | | **内存泄漏(Memory Leak)** | 比如缓存未清理、静态集合持有对象引用等,导致对象无法被回收,最终填满老年代。 | | **堆设置不合理** | 堆太小,尤其是老年代太小;或者新生代比例过大,导致老年代容易满。 | | **CMS 或 G1 配置不当** | 使用 CMS 但触发时机太晚,G1 回收不及时等。 | | **大量大对象分配** | 如缓存大数组、大 Map、文件读取未流式处理等。 | | **显式调用 System.gc()** | 第三方库或代码中手动触发 GC,可通过 `-XX:+DisableExplicitGC` 禁用。 | --- ### 🔍 如何诊断? #### 1. 查看 GC 日志(关键) 启用并分析 GC 日志: ```bash -XX:+PrintGC # 启用基本GC日志 -XX:+PrintGCDetails # 更详细信息 -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log ``` 然后使用工具分析: - **[GCViewer](https://github.com/chewiebug/GCViewer)** - **[gceasy.io](https://gceasy.io)**(上传 gc.log 即可可视化分析) 关注以下指标: - 每次 Full GC 前后老年代使用量(Old Gen Usage) - Full GC 是否能有效回收内存(回收后是否仍很高?) - 是否有 `System.gc()` 调用? - Full GC 时间长短(是否造成长时间 STW?) #### 2. 使用 jstat 实时监控 ```bash jstat -gcutil <pid> 1000 10 ``` 观察 `FGC` 列增长速度,结合时间判断频率。 #### 3. 使用 MAT 或 JProfiler 分析堆 dump 生成堆转储文件: ```bash jmap -dump:format=b,file=heap.hprof <pid> ``` 用 [Eclipse MAT](https://www.eclipse.org/mat/) 打开,查找: - 最大对象是什么? - 是否存在重复的大对象? - 是否有可疑的集合类(HashMap、List)不断增长? --- ### ✅ 示例:如何减少 Full GC 假设你使用的是 **G1GC**,可以优化 JVM 参数: ```bash # 示例JVM参数(根据实际情况调整) -Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=45 # 提前启动并发标记 -XX:+G1UseAdaptiveIHOP # 自适应IHOP -XX:-DisableExplicitGC # 禁止System.gc() -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps ``` > 关键点:让 G1 在老年代还没满之前就开始混合回收,避免退化为 Full GC。 --- ### 🛠️ 排查流程建议 1. **确认是否真的是“Full GC”** 检查日志中是 `Full GC` 还是 `Major GC`(有些日志把 G1 Mixed GC 也误标为 Full GC)。真正的 Full GC 是 **Serial Old 或 Parallel Old 的 stop-the-world 全堆回收**。 2. **检查是否有 System.gc()** ```bash -XX:+PrintGCApplicationStoppedTime -XX:+PrintReferenceGC ``` 查看是否由显式 GC 引起。 3. **查看老年代增长趋势** 若老年代内存呈线性上升,极可能是内存泄漏。 4. **对比不同时间段的堆 dump** 抓取两次间隔几小时的堆 dump,做差异对比(MAT 中的 "Compare Basket"),看哪些对象在持续增加。 --- ### ✅ 总结 | 情况 | 是否正常 | |------|----------| | 一天 11 次 Full GC + 每次回收效果差(内存没释放) | ❌ 严重问题,可能存在内存泄漏 | | 一天 11 次 Full GC + 老年代很快填满 | ❌ 需优化对象生命周期或堆大小 | | 一天 11 次 Full GC + 来自 System.gc() | ⚠️ 可禁用 | | 一天 11 次 Full GC + 实际是 G1 Mixed GC(非真正 Full GC) | ✅ 可接受,但需确认 STW 时间 | > 🔴 **结论:一天 11 次 Full GC 很可能有问题,必须结合 GC 日志和堆分析进一步确认。** --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值