java-jvm-full gc频繁的分析及解决

本文分析了Java应用程序中CMS垃圾收集器导致的频繁Full GC现象,从解决问题的角度出发,探讨了如何触发Full GC的原因,并提出了相应的解决策略。

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

 关于施用full gc频繁的分析及解决
  • 发布时间: 2013/10/13 20:32 
  • 阅读: 3431 
  • 收藏: 14 
  • 点赞: 1 
  • 评论: 1
关于应用full gc频繁的分析及解决 
很久前的工作日记了,移到ITeye上来。

现象

系统报警full gc次数过多,每2分钟达到了5~6次,这是不正常的现象
在full gc报警时的gc.log如下:
在full gc报警时的jstat如下:
sudo -u admin -H /opt/taobao/java/bin/jstat -gcutil `pgrep java` 2000 100
 
此时的cpu如下(基本都是在做gc): 
将应用重启后,问题解决
但是当后台执行低价航线更新时,过大概十几个小时后,又出现上述情况!

分析

当频繁full gc时,jstack打印出堆栈信息如下:
sudo -u admin -H /opt/taobao/java/bin/jstack `pgrep java` > #your file path#
可以看到的确是在跑低价信息
另外在应用频繁full gc时和应用正常时,也执行了如下2种命令:
sudo -u admin -H /opt/taobao/java/bin/jmap -histo `pgrep` > #your file path#
sudo -u admin -H /opt/taobao/java/bin/jmap -histo:live `pgrep` > #your file path#(live会产生full gc)
目的是确认以下2种信息:
(1)是否存在某些引用的不正常,造成对象始终可达而无法回收(Java中的内存泄漏)
(2)是否真是由于在频繁full gc时同时又有大量请求进入分配内存从而处理不过来,
        造成concurrent mode failure?
下图是在应用正常情况下,jmap不加live,产生的histo信息:
下图是在应用正常情况下,jmap加live,产生的histo信息:
下图是在应用频繁full gc情况下,jmap不加live和加live,产生的histo信息:
从上述几个图中可以看到:
(1)在应用正常情况下,图中标红的对象是被回收的,因此不是内存泄漏问题
(2)在应用频繁full gc时,标红的对象即使加live也是未被回收的,因上就是在频繁full gc时,
        同时又有大量请求进入分配内存从而处理不过来的问题
先从解决问题的角度,看怎样造成频繁的full gc?
从分析CMS GC开始
先给个CMS GC的概况:
(1)young gc
可以看到,当eden满时,young gc使用的是ParNew收集器
ParNew: 2230361K->129028K(2403008K), 0.2363650 secs解释:
12230361K->129028K,指回收前后eden+s1(或s2)大小
2)2403008K,指可用的young代的大小,即eden+s1(或s2)
3)0.2363650 secs,指消耗时间
2324774K->223451K(3975872K), 0.2366810 sec解释:
12335109K->140198K,指整个堆大小的变化
(heap=(young+old)+perm;young=eden+s1+s2;s1=s2=young/(survivor ratio+2))
2)0.2366810 sec,指消耗时间
[Times: user=0.60 sys=0.02, real=0.24 secs]解释:指用户时间,系统时间,真实时间
2cms gc
当使用CMS收集器时,当开始进行收集时,old代的收集过程如下所示:
a)首先jvm根据-XX:CMSInitiatingOccupancyFraction-XX:+UseCMSInitiatingOccupancyOnly
     来决定什么时间开始垃圾收集
b)如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了
     -XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc
c)如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定什么时候
    触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数
    没有设置的原因
d)当cms gc开始时,首先的阶段是CMS-initial-mark,此阶段是初始标记阶段,是stop the world阶段,
     因此此阶段标记的对象只是从root集最直接可达的对象
CMS-initial-mark:961330K(1572864K),指标记时,old代的已用空间和总空间
e)下一个阶段是CMS-concurrent-mark,此阶段是和应用线程并发执行的,所谓并发收集器指的就是这个,
     主要作用是标记可达的对象
此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark
f)下一个阶段是CMS-concurrent-preclean,此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的,
    因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题
因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分
    工作以节省时间
此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean
g)下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些,
     作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间
此阶段涉及几个参数:
-XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束
-XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,
即当eden使用达到此值时,才会开始abortable-preclean阶段
-XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行
此阶段会打印一些日志如下:
CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
CMS:abort preclean due to time XXX
h)再下一个阶段是第二个stop the world阶段了,即Rescan阶段,此阶段暂停应用线程,对对象进行重新扫描并
     标记
YG occupancy:964861K2403008K),指执行时young代的情况
CMS remark:961330K(1572864K),指执行时old代的情况
此外,还打印出了弱引用处理、类卸载等过程的耗时
i)再下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理
j)最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构
(3)full gc:
有2种情况会触发full gc,在full gc时,整个应用会暂停
a)concurrent-mode-failure:当cms gc正进行时,此时有新的对象要进行old代,但是old代空间不足造成的
b)promotion-failed:当进行young gc时,有部分young代对象仍然可用,但是S1或S2放不下,
    因此需要放到old代,但此时old代空间无法容纳此

 

频繁full gc的原因
从日志中可以看出有大量的concurrent-mode-failure,因此正是当cms gc进行时,有新的对象要进行old代,
但是old代空间不足造成的full gc
进程的jvm参数如下所示:
影响cms gc时长及触发的参数是以下2个:
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:CMSInitiatingOccupancyFraction=80
解决也是针对这两个参数来的
根本的原因是每次请求消耗的内存量过大

解决

1)针对cms gc的触发阶段,调整-XX:CMSInitiatingOccupancyFraction=50,提早触发cms gc,就可以
        缓解当old代达到80%,cms gc处理不完,从而造成concurrent mode failure引发full gc
2)修改-XX:CMSMaxAbortablePrecleanTime=500,缩小CMS-concurrent-abortable-preclean阶段
        的时间
3)考虑到cms gc时不会进行compact,因此加入-XX:+UseCMSCompactAtFullCollection
       (cms gc后会进行内存的compact)和-XX:CMSFullGCsBeforeCompaction=4
       (在full gc4次后会进行compact)参数
但是运行了一段时间后,只不过时间更长了,又会出现频繁full gc
计算了一下heap各个代的大小(可以用jmap -heap查看):
total heap=young+old=4096m
perm:256m
young=s1+s2+eden=2560m
young avail=eden+s1=2133.375+213.3125=2346.6875m
s1=2560/(10+1+1)=213.3125m
s2=s1
eden=2133.375m
old=1536m
可以看到eden大于old,在极端情况下(young区的所有对象全都要进入到old时,就会触发full gc),
因此在应用频繁full gc时,很有可能old代是不够用的,因此想到将old代加大,young代减小
改成以下:
-Xmn1920m
新的各代大小:
total heap=young+old=4096m
perm:256m
young=s1+s2+eden=1920m
young avail=eden+s1=2133.375+213.3125=1760m
s1=1760/(10+1+1)=160m
s2=s1
eden=1600m
old=2176m
此时的eden小于old,可以缓解一些问题
<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 内存参数?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值