记一次生产系统每隔10小时(36000000毫秒)固定进行一次Full GC排查思路

一、 背景描述

某个应用在生产环境通过系统监控发现,应用每隔10小时就会触发一次Full GC,该系统当时承接的业务量并不大,而且固定10小时就会进行Full GC,通过监控时间轴发现Full GC频率很规律,直觉告诉我这不是JVM自身触发的Full GC操作,应该是某个定时任务中进行了垃圾回收操作,但是什么业务场景会存在这种情况呢?

二、问题排查

通过在测试环境上通过系统监控可以发现,应用的JVM使用情况并未达到Full GC条件,是系统调用了System.gc()方法产生的Full GC。于是,我在代码中对System.gc()方法调用进行了搜索,甚至为了防止是通过反射方式调用的,还检索了所有业务代码,但是均未发现有业务代码进行该方法的调用。

不过,在排查过程中,Jdk中有一个GC类却引起了我的注意:

在这里插入图片描述
在Java中Daemon一般用作标识守护线程,这里有一个Daemon类,是不是因为在这个线程类中调用System.gc()方法的原因呢?

查看sun.misc.GC.Daemon类的源码:

在这里插入图片描述

通过源码可以发现,Daemon类继承了Thread线程类,并且有调用System.gc()方法。

同时,在应用中,通过jstack命令查看,应用中确实有此线程运行:

在这里插入图片描述

通过对代码进行断点调试,发现Apache cxf相关类会有周期性Full GC的问题,我引入的相关依赖如下:

        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
            <version>3.3.11</version>
        </dependency>

通过对源码解析发现,最终调用的情况如下:

  • org.apache.cxf.common.logging.LogUtils
    在这里插入图片描述
  • org.apache.cxf.common.logging.JDKBugHacks
    在这里插入图片描述
    注意这里:skipHack()方法的判断逻辑,通过这部分代码可以设置参数跳过执行创建守护线程的逻辑。
    在这里插入图片描述

三、原因分析

源码解析流程如下:LogUtils类加载时会通过静态代码块调用JDKBugHacks.doHacks()方法,此方法会获取系统环境变量org.apache.cxf.JDKBugHacks.gcRequestLatency,如果没有设置,默认为false,此时会通过反射调用sun.misc.GCrequestLatency方法,

在这里插入图片描述
初始化的时候deamon对象的值为null,此时会调用Daemon.create()方法,如下:

在这里插入图片描述
create() 方法中,会创建一个名字为 GC Daemon的守护线程,如下:

在这里插入图片描述

四、问题解决

Apache CXF是一个开源的Service框架,它实现了JCP和Web Servicez中的一些重要标准,大大简化了Web Service服务构建开发工作,通过上述对org.apache.cxf.common.logging.JDKBugHacks源码分析,可以在项目启动脚本中加入

-Dorg.apache.cxf.JDKBugHacks.gcRequestLatency=true

来跳过创建守护线程的逻辑,解决固定Full GC的逻辑。

<think> 我们正在讨论的是CMS垃圾回收器在Full GC后老年代仍无可用空间的问题。根据引用[1],CMS在并发回收期间需要预留空间(默认约8%)以支持用户线程继续分配对象到老年代。当老年代空间不足时,会发生并发模式失败(Concurrent Mode Failure),导致退化为Serial Old Full GC。现在的问题是:即使进行了10M的Full GC(这里可能是指老年代空间为10MB,或者Full GC清理了10MB空间?),但老年代仍然没有可用空间。我们需要分析原因并提供解决方案。 关键点: 1. 老年代空间过小,导致预留空间不足,容易触发并发模式失败。 2. 即使进行了Full GC,可能由于内存碎片或者对象无法被回收(比如有强引用),导致空间无法释放。 3. 另外,CMS在并发标阶段可能漏掉某些对象(因为并发标期间用户线程还在运行),所以在重新标阶段会进行修正,但是最终如果发现所有对象都是存活的,那么就无法回收空间。 可能的原因及解决办法: ### 一、原因分析 1. **内存碎片**:CMS采用标-清除算法,不进行内存压缩(除非配置了压缩),导致老年代产生大量碎片。当需要分配一个大对象时,即使总剩余空间足够,也无法找到连续空间,从而触发Full GC。而Full GC(Serial Old)会进行压缩,但如果压缩后空间仍然不足,则说明老年代确实被占满。 2. **对象存活率高**:老年代中存活对象过多,且都是强引用,无法被回收。即使进行Full GC,也无法回收空间。 3. **永久代/元空间不足**:在JDK7及之前,永久代空间不足也会触发Full GC,但这里我们关注老年代。 4. **System.gc()调用**:显式调用System.gc()可能导致Full GC,且会忽略CMS的并发回收,直接进行Serial Old GC。 ### 二、解决方案 #### 1. 调整老年代大小 - 增加堆内存:通过`-Xmx`和`-Xms`增加堆大小,从而增加老年代空间。 - 调整新生代和老年代的比例:通过`-XX:NewRatio`(例如`-XX:NewRatio=3`表示老年代与新生代的比例为3:1)来增大老年代。 #### 2. 优化CMS垃圾回收器参数 - **降低CMS触发阈值**:通过`-XX:CMSInitiatingOccupancyFraction`设置更早触发CMS回收,避免老年代过满。例如: ```bash -XX:CMSInitiatingOccupancyFraction=60 # 当老年代使用60%时触发CMS ``` 同时,可以配合使用`-XX:+UseCMSInitiatingOccupancyOnly`来确保这个阈值被使用。 - **启用CMS内存压缩**:在Full GC时进行内存碎片整理(默认在几次Full GC后才会压缩一次)。可以设置每次Full GC都压缩: ```bash -XX:+UseCMSCompactAtFullCollection # 在Full GC时进行压缩 -XX:CMSFullGCsBeforeCompaction=0 # 每次Full GC后都压缩内存 ``` - **避免晋升失败**:确保新生代有足够空间,避免大量对象过早晋升到老年代。可以调整新生代大小(`-Xmn`)或调整Survivor区比例(`-XX:SurvivorRatio`)。 #### 3. 排查内存泄漏 - 如果Full GC后老年代空间没有释放,可能存在内存泄漏。使用内存分析工具(如MAT、VisualVM)分析堆转储(通过`jmap`获取)以确定是否有对象异常占用内存。 #### 4. 避免大对象直接进入老年代 - 调整大对象阈值:通过`-XX:PretenureSizeThreshold`设置大对象直接进入老年代的阈值,避免过大的对象直接进入老年代导致空间紧张。例如: ```bash -XX:PretenureSizeThreshold=1M # 只有大于1MB的对象才直接分配在老年代 ``` #### 5. 升级垃圾回收器 - 如果问题依然存在,考虑升级到G1垃圾回收器。G1将堆划分为多个Region,通过并发标和并行清理,同时进行部分压缩,减少碎片问题。启动G1: ```bash -XX:+UseG1GC ``` ### 三、配置示例 假设堆总大小为512MB,老年代希望设置得大一些,同时使用CMS并启用内存压缩: ```bash java -Xms512m -Xmx512m -XX:NewRatio=3 -XX:+UseConcMarkSweepGC \ -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseCMSInitiatingOccupancyOnly \ -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 \ -XX:PretenureSizeThreshold=1M \ -jar your-application.jar ``` ### 四、监控与验证 - 开启GC日志以监控GC行为: ```bash -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log ``` 分析GC日志,确认Full GC的原因(如并发模式失败、晋升失败等)以及回收后的空间情况。 §§相关问题§§ 1. 如何分析CMS的GC日志以识别内存碎片问题? 2. 在CMS垃圾回收器中,如何避免并发模式失败? 3. G1垃圾回收器如何解决CMS中的内存碎片问题? 4. 如何设置JVM参数以获取详细的GC日志? 5. 除了调整堆大小,还有哪些方法可以优化老年代的垃圾回收? 引用说明: - 引用[1]:解释了CMS触发Full GC的条件(默认92%),以及预留空间的作用。 - 引用[2]:介绍了CMS的设计目标(低停顿时间)。 - 引用[3]:说明了CMS并发标阶段的工作机制(与用户线程并发运行)。 </think>### JVM CMS垃圾回收器:Full GC后老年代仍无可用空间的解决方案 #### 一、问题本质分析 当CMS执行10MB Full GC后老年代仍无空间,核心问题在于: 1. **内存碎片化严重** CMS标-清除算法不整理内存,导致产生大量内存空洞,即使总空闲空间$ \geq 10\text{MB} $,但无连续空间分配对象(如5MB大对象)[^1]: ```mermaid graph LR A[内存状态] --> B[碎片1:2MB] A --> C[碎片2:3MB] A --> D[碎片3:5MB] D -.-> E[需分配6MB对象?] E -->|无连续空间| F[Allocation Failed] ``` 2. **对象存活率过高** Full GC后存活对象仍占满老年代,常见原因: - 内存泄漏(如静态集合持续增长) - 缓存设计不合理(如无过期机制) - 元数据/metaspace占用过高 3. **并发模式失败连锁反应** 引用[1]指出预留空间不足时,CMS会频繁触发Concurrent Mode Failure,导致Serial Old GC雪崩式发生。 --- #### 二、针对性解决方案 ##### 1. 解决内存碎片问题 ```bash # 强制每次Full GC进行内存压缩 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 # 每次Full GC都压缩 # 启用并行压缩减少STW时间 -XX:+CMSCompactWhenClearAllSoftRefs -XX:ParallelGCThreads=4 # 根据CPU核心数调整 ``` > **效果**:压缩10MB老年代通常需$ \text{5-20ms} $ STW时间,远低于无空间导致的系统冻结。 ##### 2. 降低对象晋升速度 ```bash # 增大新生代空间比例 -XX:NewRatio=2 # 老年代:新生代=2:1 # 调整Survivor区避免过早晋升 -XX:SurvivorRatio=8 # Eden:Survivor=8:1:1 -XX:MaxTenuringThreshold=10 # 提升对象在新生代存活次数 ``` ##### 3. 优化CMS触发策略 ```bash # 降低触发阈值避免空间耗尽 -XX:CMSInitiatingOccupancyFraction=50 # 老年代50%占用即触发 # 启用动态阈值调整 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyPrecent=50 ``` ##### 4. 内存泄漏排查(关键步骤) 使用以下命令检测泄漏对象: ```bash # 生成堆转储 jmap -dump:live,format=b,file=heap.bin <pid> # 分析堆转储(MAT工具) java -jar mat.jar heap.bin ``` 排查重点: - 静态集合类(如`static HashMap`) - 线程局部变量未清理 - 未关闭的资源(数据库连接/文件流) --- #### 三、配置优化示例 针对10MB老年代场景的完整参数: ```bash java -Xmx20m -Xms20m \ -XX:NewRatio=2 \ # 老年代≈13MB -XX:+UseConcMarkSweepGC \ -XX:CMSInitiatingOccupancyFraction=50 \ -XX:+UseCMSCompactAtFullCollection \ -XX:CMSFullGCsBeforeCompaction=0 \ -XX:PretenureSizeThreshold=1m \ # >1MB对象不进老年代 -XX:+HeapDumpOnOutOfMemoryError \ -jar your_app.jar ``` --- #### 四、终极方案:升级垃圾回收器 当老年代$ \leq 10\text{MB} $时,建议迁移至G1或ZGC: ```bash # G1解决方案(自动处理碎片) -XX:+UseG1GC -XX:G1HeapRegionSize=1m # 设置Region大小 # ZGC解决方案(亚毫秒停顿) -XX:+UseZGC -XX:ZAllocationSpikeTolerance=5.0 # 容忍内存分配尖峰 ``` > **优势对比**: > | 回收器 | 碎片处理 | 10MB堆表现 | > |--------|----------|------------| > | CMS | 需手动压缩 | STW $ \geq 200\text{ms} $ | > | G1 | 自动压缩 | STW $ \leq 10\text{ms} $ | > | ZGC | 指针压缩 | STW $ \leq 1\text{ms} $ | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder-文小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值