一次性能测试中JVM频繁Full GC的解决经验总结

本文分析了Java应用在高并发场景下出现性能波动的原因,通过深入研究JVM内存管理和调优策略,解决了由频繁FullGC导致的性能瓶颈。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题现象

         最近对平台功能进行性能优化。功能完成后,开始在环境进行长稳测试。使用模拟客户端以每秒15000TPS向系统发送请求,结果发现系统性能总是保持一段时间后开始下降,最坏的情况只有8000到9000TPS左右,之后开始上升下降反复来回。

                                   

 

问题分析

         系统由java编写,性能出现锯齿形波动的现象,猜测可能是由于java虚拟机(JVM)出现Full GC(垃圾回收)现象导致。我们知道,当JVM进行Full GC时,进程会出现暂停响应(stop the world)的现象。为了确认这一点,我在java启动命令行参数中加上输出gc日志的参数。

         XX:+PrintGCDetails -Xloggc:dcc_gc.log

         通过gc日志,我们可以明显看到,当测试进行一段时间后,JVM发生了Full GC,而且次数比较频繁,直接影响性能测试。可以看到下图中有Full GC的日志。

   

       JVM触发Full GC的条件是什么呢?下面先介绍下java一些知识。

       JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)。

                              

       上图是java运行时数据存储模型,包括栈,堆,方法区等等。程序内存数据一般都在堆中进行管理。Java堆又分为新生代和老年代,新生代可以分为Eden、Survivor。新建的对象会存放到新生代中,当多次垃圾回收后,仍然存活的对象会转移到老年代中。因此对象存活路径为:Eden->Survivor->Old Generation。

                             

       当老年代空间不足时,这时就会发生Full GC。明白了JVM内存管理机制,下面就开始进行优化。

第一次优化

       那么上面性能测试现象是不是因为内存不够导致的呢。我又对命令行参数添加内存设置参数:

      -Xmx30g -Xms30g -Xmn4g

      Xmx:进程最大内存,这里设置为30G。

      Xms:进程初始内存,和Xmx设置一样。

      Xmn:新生代大小,设置为4g。

      通过以上设置,老年代的内存大小为30-4=26G,已经足够大了。继续测试发现,Full GC频率有所下降,而且GC时间比较短。但是Full GC现象还是会间隔发生,并会影响响应时长。(单位为us秒)。

                             

 

         看来通过进程内存参数设置,不能从根本上解决问题。

第二次优化

         既然参数调整没法解决问题,那么只能深层次分析内存中到底是哪些对象占用了如此大的内存。这时,需要使用到几个java内存分析工具:jmap,MAT等。

         首先使用jmap工具,导出整个JVM 中内存信息。命令如下:

         jmap -dump:format=b,file=文件名 [pid]

         由于进程内存比较大,dump过程比较缓慢,耐心等待就好。大概等待20分钟后,终止导出来30G大小的dump文件。由于文件太大,一般jvisualvm分析工具难以加载。经过查阅资料,我是用MAT工具对dump文件进行分析。

        ./ParseHeapDump.sh m.hprof  org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

        m.hprof就是jvm的dump文件,在mat目录下会生成3份.zip结尾的报告和一些m.相关的文件,将生成的m.hprof相关的文件都下载到windows本地磁盘。由于dump文件比较大,以上过程进行比较缓慢,因此可以放到linux后台运行。以下是生成的报告内容。

                  

        在报告中,我发现RingBuff这个实例特别奇怪,只有一个实例,但是占用的空间非常大。为此,这时我开始查阅程序源代码。

        通过走读源代码,了解到系统是基于Netty+Disruptor框架编写。Disruptor是一个开源框架,并发性能非常强悍。它使用了一个叫RingBuffer环形队列的数据结构,避免使用同步锁,因此性能非常高。

                                      

        RingBuffer环形队列里面的数据不会释放,当队列满时会覆盖前面的数据。这样一来,随着系统的运行最终肯定有buffersize大小的数据长驻内存中,不会被垃圾回收器回收。

         经过以上分析,我立即查看环境中RingBufferSize配置,果不其然,这个参数被设置的很大:6291456。意味着整个环形队列有6291456个槽位,而RingBuffer里面存放的是Cdr话单对象,这个对象里面有个HashMap,可能包含几十到上百个话单字段。如果一个话单对象有几百个字节,那么整个队列将占有几G甚至几十G的内存。怪不得老年代内存不够用,会频繁发生GC。

        综合考虑性能测试目标,我将RingBufferSize设置为2048,经过近1亿数据量的测试,终于再没有出现Full GC的情况。

 

<think>首先,用户的问题是关于Java应用中频繁触发Full GC的排查和解决。我需要基于系统级指令和用户可见层的要求来构建回答。 系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 - 参考站内引用(提供的引用[^1]到[^5])。 参考引用内容: - [^1]:JVM频繁Full GC问题的排查与解决方案。包括配置JVM参数、优化GC算法、减少对象创建、避免内存泄漏等。 - [^2]:频繁full gc问题排查及解决。包括监控Full GC、使用jstat、分析dump文件、检查大对象如List<XXXDTO>、避免查询大量数据等。 - [^3]:线上问题排查:如果JVM出现频繁FullGC该如何解决?。包括空间分配担保机制检查。 - [^4]:java中什么情况会触发FullGC,如何避免频繁FullGC。包括碎片化、OOM风险、对象晋升失败等。 - [^5]:java 频繁 full gc_java-jvm-full gc频繁的分析及解决。包括CMS参数如-XX:CMSScheduleRemarkEdenSizeThreshold。 用户意图:了解或解决Java应用中频繁触发Full GC的问题,可能涉及内存泄漏、调优或垃圾回收机制分析。 我的回答结构: 1. **介绍问题**:简要解释Full GC及其影响。 2. **逐步排查方法**:提供清晰的步骤,帮助用户排查问题。 3. **解决方案**:基于排查结果,给出优化建议。 4. **生成相关问题**:在末尾添加相关问题。 使用中文回答,并确保结构清晰。 在回答中,自然地引用段落。引用时,在末尾添加引用标识如[^1]。例如,如果提到配置JVM参数,可以引用[^1]。 数学表达式:由于主题是Java和GC,可能没有太多数学内容。但如果有相关公式,比如垃圾回收算法中的公式,我会使用LaTeX格式。例如,如果提到堆大小计算,可以用$...$。但在这个上下文中,可能不需要数学表达式,所以我将专注于文本内容。 生成相关问题:在回答末尾,添加3-5个相关问题,以"§§相关问题§§"开头,类似于示例。 完整回答大纲: 1. **引言**:解释Full GC及其危害。 2. **排查步骤**: - 监控Full GC情况。 - 使用工具分析(如jstat、dump文件)。 - 检查常见原因(如内存泄漏、大对象)。 3. **解决方案**: - JVM参数调优。 - 代码优化。 - 避免常见陷阱。 4. **结论**:总结关键点。 5. **相关问题**:列出相关问题。 确保引用自然:当提到特定点时,引用相关来源。例如: - "通过合理配置JVM参数可以减少Full GC[^1]." - "分析dump文件可以识别大对象[^2]." 最终输出格式: - 回答主体。 - 以"§§相关问题§§"开头的问题列表。</think>### Java应用频繁触发Full GC的排查与解决 Full GCFull Garbage Collection)是Java虚拟机(JVM)垃圾回收过程中的一个关键阶段,它会暂停所有应用线程(Stop-The-World,STW),导致系统性能下降、响应延迟甚至服务中断。频繁触发Full GC通常表明内存管理存在问题,如内存泄漏、对象分配不当或JVM配置不合理。以下是基于行业最佳实践的逐步排查和解决方案,帮助您快速定位并解决问题。所有建议均参考可靠的JVM文档和实战经验,确保真实可靠[^1][^2][^3]。 --- ### 一、Full GC的常见原因和影响 Full GC通常在以下场景触发: - 老年代空间不足(如对象晋升失败或内存碎片化)。 - 系统主动调用`System.gc()`。 - JVM空间分配担保机制失败(老年代连续空间不足)。 - 新生代GC后存活对象过多,无法放入Survivor区。 频繁Full GC的危害包括: - **性能下降**:STW暂停时间增加,影响应用吞吐量和响应时间。 - **内存溢出风险**:碎片化可能导致OOM(OutOfMemoryError),即使总内存充足[^4]。 - **资源浪费**:CPU和内存利用率异常升高。 --- ### 二、逐步排查方法 排查应遵循“监控→分析→定位”流程,使用标准JVM工具。 #### 步骤1: 监控Full GC频率和内存使用 - **使用JVM内置工具**: - 通过`jstat -gc <pid> <interval>` 实时查看GC统计,关注`FGC`(Full GC次数)和`FGCT`(Full GC总时间)。例如: ```bash jstat -gc 27928 1000 # 每秒监控一次进程27928的GC情况 ``` 输出关键列:`OC`(老年代总容量)、`OU`(老年代已使用量)。如果`OU`接近`OC`且`FGC`持续增长,表明老年代压力大[^2]。 - 启用GC日志:在JVM启动参数中添加: ```bash -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log ``` 分析日志中的`Full GC`事件,记录暂停时间和原因(如"Allocation Failure")。 - **监控平台**:如果公司有APM工具(如Prometheus、Zabbix),设置Full GC频率告警阈值(如每分钟超过1次)[^2]。 #### 步骤2: 分析内存对象和泄漏 - **生成堆转储(Heap Dump)**: - 在Full GC频繁时触发dump: ```bash jmap -dump:live,format=b,file=/path/to/heapdump.hprof <pid> ``` - 使用分析工具(如Eclipse MAT或VisualVM)加载dump文件: - 检查"Dominator Tree",识别占用内存最大的对象(如大数组或集合)。 - 查找可疑对象:常见问题包括未分页的数据库查询结果(如`List<XXXDTO>`)、缓存未清理或静态集合持续增长[^2][^4]。 - **代码定位**: - 在MAT中,通过"Merge Shortest Paths to GC Roots"功能追踪对象引用链。 - 重点检查:频繁创建对象的代码块、大查询(如SQL未加`LIMIT`)、或内存泄漏模式(如监听器未注销)[^2]。 #### 步骤3: 检查JVM配置和GC机制 - **验证空间分配担保**: - JVM在Minor GC前会检查老年代空间是否足够(参数`-XX:+HandlePromotionFailure`)。 - 如果老年代最大连续空间小于历次晋升对象的平均大小,会触发Full GC[^3]。 - 使用`jinfo <pid>`查看当前JVM参数,确认`-XX:MaxTenuringThreshold`(对象晋升阈值)是否合理。 - **评估GC算法**: - CMS或G1等并发收集器可能因碎片化导致Full GC。检查参数如`-XX:CMSScheduleRemarkEdenSizeThreshold`(默认2MB),它影响CMS的预处理阶段[^5]。 - 计算碎片率:如果老年代使用率低但Full GC频繁,碎片化可能是主因[^4]。 --- ### 三、解决方案 根据排查结果,针对性优化: #### 1. JVM参数调优(减少Full GC触发) - **调整堆大小和比例**: - 增大老年代:设置`-Xmx`(最大堆)和`-Xms`(初始堆)一致,避免动态扩容。例如: ```bash -Xms4g -Xmx4g -XX:NewRatio=2 # 老年代占堆的2/3 ``` - 优化Survivor区:增加`-XX:SurvivorRatio=8`,减少对象过早晋升。 - **优化GC算法**: - 切换到G1收集器(适合大堆):添加`-XX:+UseG1GC`,并设置目标暂停时间: ```bash -XX:MaxGCPauseMillis=200 # 目标暂停200ms ``` - 针对CMS:减少碎片化风险: ```bash -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 # 每次Full GC后压缩 ``` - **禁用无效调用**: - 禁止主动GC:`-XX:+DisableExplicitGC`(忽略`System.gc()`)。 - 启用空间担保:`-XX:+HandlePromotionFailure`(避免不必要的Full GC)[^3]。 #### 2. 代码和架构优化 - **减少对象创建**: - 避免在循环中创建大对象;使用对象池(如Apache Commons Pool)。 - 优化查询:添加分页(`LIMIT`)和条件过滤,防止加载过量数据到内存[^2]。 - **修复内存泄漏**: - 清理静态集合:使用`WeakHashMap`或定期`clear()`。 - 关闭资源:确保数据库连接、流等实现`AutoCloseable`。 - **监控和预防**: - 定期执行压力测试,使用Profiler(如Arthas)实时诊断。 - 添加监控:在Spring Boot中集成Micrometer,跟踪GC指标[^1]。 #### 3. 紧急处理 如果线上系统已受影响: - 临时扩容堆内存(`-Xmx`)。 - 重启实例,但需优先解决根本原因。 --- ### 四、效果验证和最佳实践 - **验证方法**:优化后,监控`jstat`输出和GC日志,Full GC频率应下降50%以上,暂停时间缩短。 - **最佳实践**: - 开发阶段:代码审查时检查集合使用;测试环境模拟高负载。 - 参考案例:某电商通过修复一个未分页的DTO查询,Full GC从每小时10次降至0次[^2]。 - 原理回顾:Full GC开销可表示为$T_{\text{full}} \propto \text{堆大小} / \text{GC吞吐量}$,优化核心是减少对象驻留时间[^1][^4]。 通过以上步骤,您能有效解决频繁Full GC问题。如果问题复杂,建议结合多个工具深入分析[^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值