JVM频繁Full GC的情况及应对策略

本文探讨了导致JVM频繁Full GC的原因,包括老年代和永生区空间不足、CMS GC问题、Minor GC晋升平均大小超过老年代剩余空间以及大对象分配。针对这些问题,提出了相应的优化策略,如调整内存空间、禁用System.gc()调用以及配置CMS GC参数以减少Full GC的频率和影响。

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略。

 

1、System.gc()方法的调用

 

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。


2、老年代代空间不足


老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space 
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

3、永生区空间不足


JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space 
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。


4、CMS GC时出现promotion failed和concurrent mode failure


对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能

会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在

执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕

后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。


5、统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间


这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之

前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,

则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否

大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -

Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

 

6、堆中分配很大的对象

 

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

<think>我们正在解决JVM频繁Full GC的问题。根据引用内容,频繁Full GC通常与内存设置、GC策略、内存泄漏、代码优化等有关。我们将从原因分析到解决方案逐步展开。 引用[1]提到:频繁Full GC与内存设置不当、垃圾回收器配置不合适、内存泄漏或代码优化不足有关。 引用[2]给出了一些JVM参数调整建议,并提到使用G1收集器可能有助于减少Full GC。 引用[3]指出根本原因可能包括:请求速率过高、Minor GC频繁导致对象提前晋升、大量临时对象、线程池配置不合理等。 引用[4]则介绍了一个关于SoftReference的参数。 因此,我们将从以下几个方面进行分析和优化: 1. 内存设置与GC策略调整 2. 内存泄漏排查 3. 代码优化 4. 其他配置优化 下面详细展开:</think>### JVM 频繁 Full GC 的原因分析与优化方案 --- #### **一、核心原因分析** 根据引用内容,频繁 Full GC 的根本原因可分为四类: 1. **内存配置不当** - 堆内存分配不合理(新生代过小导致对象提前晋升老年代)[^3] - 元空间溢出:`Metaspace` 不足触发 Full GC[^2] - 大对象直接进入老年代(如未分片的超大数组) 2. **GC 策略缺陷** - 使用 CMS/Parallel GC 处理大堆时 STW 时间长[^2] - 未启用自适应调节策略(如 `-XX:+UseAdaptiveSizePolicy`) 3. **内存泄漏** - 静态集合类持有对象引用(如 `static Map`) - 未关闭的资源(数据库连接、文件流) - 线程局部变量未清理(`ThreadLocal` 滥用) 4. **代码与并发问题** - 高频创建临时对象(如日志拼接、JSON 序列化)[^3] - 线程池配置过大:线程栈总内存超过新生代容量 - 请求速率超过系统处理能力(对象创建速度 > GC 回收速度)[^3] --- #### **二、优化解决方案** ##### **1. 内存与 GC 参数调优** ```bash # 基础配置(示例:8G 堆内存应用) -Xms8g -Xmx8g # 堆内存固定避免震荡 -Xmn4g # 新生代占堆50%(推荐比例) -XX:MetaspaceSize=512m # 元空间初始大小[^2] -XX:MaxMetaspaceSize=1g # 防止元空间膨胀 -XX:+UseG1GC # G1 替代 CMS/Parallel[^2] -XX:MaxGCPauseMillis=200 # 目标停顿时间 -XX:InitiatingHeapOccupancyPercent=45 # 更早启动混合GC ``` **关键参数说明**: - **新生代比例**:新生代过小会导致 Minor GC 频繁,对象过早晋升老年代。设 $S_{\text{new}}$ 为新生代大小,$S_{\text{heap}}$ 为堆大小,建议: $$ S_{\text{new}} \geq 0.4 \times S_{\text{heap}} $$ - **G1 优势**:通过 Region 分片和增量回收,减少 Full GC 触发概率[^2] ##### **2. 内存泄漏排查** - **步骤 1**:生成内存快照 ```bash jmap -dump:live,format=b,file=heap.hprof <pid> ``` - **步骤 2**:分析支配树(MAT/Eclipse Memory Analyzer) - 查找 `java.lang.ThreadLocal` 或静态集合类 - 检查 `SoftReference` 对象存活时间(调整 `-XX:SoftRefLRUPolicyMSPerMB`[^4]) - **步骤 3**:监控对象分配 ```bash jstat -gcutil <pid> 1000 # 每1秒输出GC统计 ``` ##### **3. 代码与架构优化** - **避免临时对象**: - 用 `StringBuilder` 替代字符串拼接 - 对象池化(如 Apache Commons Pool) - **线程池改造**: ```java // 错误示范:无界队列导致内存堆积 Executors.newCachedThreadPool(); // 正确方案:限制队列长度 new ThreadPoolExecutor(core, max, 60s, new ArrayBlockingQueue<>(1000)); ``` - **请求限流**: - 当 QPS > 系统阈值时,启用熔断(如 Sentinel/Hystrix) ##### **4. 监控与应急措施** - **实时预警**: - 监控 `jvm_gc_pause_seconds_max{action="end of major GC"}` - 规则:`Full GC 频率 > 1次/分钟` 触发告警 - **应急方案**: - 临时扩容堆内存:`kill -3 <pid> && jmap -heap <pid>` - 重启时添加参数:`-XX:+ExplicitGCInvokesConcurrent`(禁止 System.gc() 触发 Full GC) --- ### 优化效果验证 1. **监控指标**: - Full GC 频率下降至 < 1次/天 - 老年代内存占用曲线平稳(无持续上升) 2. **压测验证**: - 模拟峰值流量(如 JMeter),观察 Full GC 触发条件 > **核心原则**: > - 优先解决**内存泄漏**(治本) > - 参数调优是**临时手段**(治标)[^1] > - 高并发场景需结合**限流+代码优化**[^3] --- ### 相关问题 1. 如何通过 GC 日志分析 Full GC 的具体原因? 2. G1 垃圾回收器的调优参数有哪些最佳实践? 3. 哪些工具可以实时监控 JVM 内存泄漏? 4. 如何区分 Full GC 是由内存泄漏还是流量突增引起的? 5. 在容器化环境(如 Kubernetes)中如何配置 JVM 内存参数?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值