“堆内存持续占用高 且 ygc回收效果不佳” 排查处理实践
字数 1648 2025-08-11 08:35:44
Java堆内存问题排查与优化实践
一、问题背景
自建工具运行一段时间后出现:
- 内存占用高触发报警
- 频繁Young GC且效果不佳
- 堆内存1018M,使用达到950M左右触发Young GC
- YGC之后内存占用630M,未发生Full GC
二、环境配置
容器配置
- 操作系统:Linux amd64
- CPU:2核
- JRE版本:1.8.0_191
- 物理内存:251.4GB
- JVM启动参数关键部分:
-Xms1024m -Xmx1024m -Xmn384m -XX:MetaspaceSize=64m -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -XX:ParallelGCThreads=2 -XX:CICompilerCount=2 -XX:MaxDirectMemorySize=128m
GC配置
- Full GC:PS MarkSweep
- Young GC:PS Scavenge
三、问题分析
1. Young GC机制
- 触发时机:新生代Eden区满时触发
- 回收算法:复制算法
- 问题现象:YGC后堆内存使用率仍很高(从950M降到630M)
2. Full GC机制
触发条件:
- 老年代可用连续空间 < 新生代历次YGC后升入老年代对象的平均大小
- YGC后需要放入老年代的对象超过老年代剩余空间
- 老年代内存使用率超过阈值
- 元空间不足
3. 内存占用高可能原因
- 新生代过小导致大对象直接进入老年代
- 内存泄漏
- 对象生命周期不合理
四、GC回收器详解
1. 吞吐量优先回收器
- Parallel Scavenge:新生代收集器,侧重吞吐量控制
- 吞吐量 = 运行用户代码时间/(运行用户代码时间+GC时间)
2. Serial Old
- Serial收集器的老年代版本
- 单线程,标记-整理算法
- 在服务端应用中可能成为性能瓶颈
3. Parallel Old
- Parallel Scavenge的老年代版本(JDK6+)
- 多线程并发收集,标记-整理算法
4. ParNew收集器
- Serial收集器的多线程版本
- 能与CMS收集器配合工作
5. CMS(并发标记清除)回收器
- 目标:获取最短回收停顿时间
- 算法:标记-清除
- 特点:
- 第一款真正意义上的并发收集器
- 垃圾收集线程与用户线程几乎同时运行
- 缺点:
- 对CPU资源敏感
- 会产生内存碎片
- 默认情况下不会进行内存压缩
五、排查工具与命令
1. GC日志配置
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/export/Logs/com/gc.log
-XX:+UseGCLogFileRotation
-XX:GCLogFileSize=10M
-XX:NumberOfGCLogFiles=10
-XX:+HeapDumpAfterFullGC
-XX:HeapDumpPath=/export/Logs/com/fgcdump.log
-XX:+HeapDumpOnOutOfMemoryError
2. 关键JVM参数
-XX:+UnlockExperimentalVMOptions
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-XX:+CMSParallelRemarkEnabled
3. 常用诊断命令
jstack(线程分析)
- 功能:生成线程快照,定位线程停顿原因
- 应用场景:
- 死锁分析
- CPU过高分析
- 使用示例:
jstack <pid>
jstat(JVM统计监控)
- 功能:实时监控JVM资源和性能
- 常用指标:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 81.75 24.57 94.43 95.36 92.41 5649 82.696 2 0.649 83.346- GCT = YGCT + FGCT
jmap(内存分析)
jmap -histo:堆内存对象统计直方图jmap -heap:堆内存使用情况jmap -dump:生成堆快照
4. Visual VM分析
- 装入堆dump文件分析
- OQL查询示例:
select s from java.lang.String s where s.count >= 100
六、调优方案
方案1:增大新生代
- 原配置:-Xmn384m
- 调整后:-Xmn480m
- 效果分析:
25.939: [Full GC (Metadata GC Threshold) [PSYoungGen: 4133K->0K(461312K)] [ParOldGen: 34773K->36327K(557056K)] 38907K->36327K(1018368K), [Metaspace: 62967K->62967K(1105920K)], 0.3564880 secs]- 新生代总容量:461312K (~450M)
- 老年代容量:557056K (~544M)
- 堆总容量:1018368K (~994M)
方案2:增大元空间 + CMS & ParNew
-Xms1024m -Xmx1024m
-Xmn480m
-Xss512k
-XX:MetaspaceSize=128m
-XX:ParallelGCThreads=2
-XX:CICompilerCount=2
-XX:MaxDirectMemorySize=256m
-XX:+UnlockExperimentalVMOptions
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-XX:+CMSParallelRemarkEnabled
方案3:对象优化
- 减少大对象使用
- 优化对象生命周期
- 批量处理降低对象大小
七、关键知识点总结
-
GC触发条件:
- YGC:Eden区满时触发
- FGC:老年代空间不足、元空间不足等
-
内存分配策略:
- 大对象可能直接进入老年代
- 长期存活对象会从新生代晋升到老年代
-
GC算法选择:
- 吞吐量优先:Parallel Scavenge + Parallel Old
- 低延迟优先:ParNew + CMS
-
性能指标:
- 吞吐量 = 用户代码时间/(用户代码时间+GC时间)
- 停顿时间:单次GC持续时间
-
排查流程:
- 监控GC日志
- 分析内存dump
- 检查线程状态
- 逐步调整参数验证效果
-
参数禁忌:
- 谨慎使用
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction保持默认(0)
- 谨慎使用
通过系统化的监控、分析和参数调整,可以有效解决堆内存占用高和GC效率低下的问题。