"生产环境Full GC频繁,RT超过3秒,该如何优化?" "面试官:讲讲你对CMS GC的理解?" "为什么堆内存还有40%,却频繁发生GC?"
如果你也曾被这些问题困扰,那这篇文章正是为你准备的。
开篇:一个真实的生产事故
凌晨3点,运维紧急电话打来:
"线上订单系统响应超时,监控面板一片飘红..."
登录系统一看,触目惊心:
- GC频率: 2分钟一次Full GC
- RT: P99从200ms飙升到3000ms
- CPU: GC线程占用80%
- 内存: Old区使用率不断攀升
这是一个典型的GC问题,但背后的原因可能有很多:
- 内存泄漏?
- 代码缺陷?
- GC参数配置不当?
- 还是别的什么原因?
让我们通过9个真实案例,深入探究CMS GC的各种问题场景,建立系统的GC问题诊断和优化方法。
为什么要写这篇文章?
在我10年+的Java开发生涯中,遇到最多的就是GC问题。每次面试必问的也是GC。但网上的GC文章要么太浅,要么太深,很少有完整的案例分析。
这篇文章将从实战出发:
- 9个真实案例剖析
- 每个案例都包含问题表现、底层原因、最佳实践
- 配合源码级讲解
- 完整的优化方案
无论你是初学者还是老手,都能从这篇文章中获得成长。
阅读指南
本文较长(预计阅读时间30分钟),建议:
- 先看目录,选择感兴趣的场景
- 准备好JVM工具:jstat、jmap、MAT等
- 动手实践,准备测试程序
- 收藏本文,便于日后查阅
让我们开始这段探索之旅吧!
九种CMS GC问题分析与解决方案详解
一、动态扩容引起的空间震荡
问题表现
- 服务刚启动时GC次数较多
- 最大空间有剩余但仍发生GC
- GC Cause显示"Allocation Failure"
- 每次GC后堆内存空间会被调整
底层原因
// JVM动态扩容的核心代码
void ConcurrentMarkSweepGeneration::compute_new_size() {
// 如果增量收集失败,直接扩容到最大值
if (incremental_collection_failed()) {
grow_to_reserved();
return;
}
// 根据当前使用情况计算新的大小
CardGeneration::compute_new_size();
}
问题在于:
- -Xms和-Xmx设置不一致
- 初始化时只分配-Xms大小空间
- 每次空间不足时要向OS申请内存
- 申请内存过程中必然触发GC
解决方案
- 设置-Xms和-Xmx相等,避免动态扩容
- 合理设置-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio
- 预估容量,给JVM足够的初始空间
二、显式GC的去与留 ?
问题表现
- System.gc()导致的Full GC
- 频繁GC但每次回收量不大
- GC日志中出现"System.gc()"
底层原因
public static void gc() {
boolean shouldRunGC;
synchronized(LOCK) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runFinalization();
}
}
if (shouldRunGC) {
runGC();
}
}
显式GC的问题:
- 会触发Full GC,STW时间长
- 打断CMS的并发收集
- 影响系统的吞吐量
解决方案
- 通过-XX:+DisableExplicitGC禁用System.gc()
- 使用-XX:+ExplicitGCInvokesConcurrent将System.gc()转为CMS GC
- 代码优化,避免调用System.gc()
三、MetaSpace区OOM
问题表现
- MetaSpace空间耗尽
- 出现OOM异常
- 伴随着频繁的Full GC
底层原因
// MetaSpace的类加载机制
class ClassLoader {
protected Class> loadClass(String name)
throws ClassNotFoundException {
// 1. 查找已加载的类
Class> c = findLoadedClass(name);
if (c == null) {
// 2. 尝试加载新类
c = findClass(name);
// 3. 存储到MetaSpace
defineClass(name, b, 0, b.length);
}
return c;
}
}
MetaSpace问题常见原因:
- 动态生成类导致类元数据信息过多
- ClassLoader泄漏
- 反射、动态代理使用不当
解决方案
- 设置合理的MetaSpace大小
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
- 排查类加载和卸载情况
- 检查是否存在ClassLoader泄漏
- 合理使用动态代理等反射机制
四、过早晋升
问题表现
- Young GC频繁
- 对象过早进入老年代
- 老年代空间增长过快
底层原因
// 动态年龄计算
if (age < MaxTenuringThreshold) {
// 计算当前age及以下的对象总大小
size_t total = 0;
for (int i = age; i >= 0; --i) {
total += sizes[i];
if (total > survivor_capacity/2) {
result = i;
break;
}
}
}
过早晋升原因:
- Survivor空间过小
- MaxTenuringThreshold设置过小
- 动态年龄判定导致过早晋升
解决方案
- 调整新生代大小
-XX:NewRatio=4
-XX:SurvivorRatio=8
- 调整对象晋升年龄
-XX:MaxTenuringThreshold=15
- 关注动态年龄计算
- 检查是否存在大对象直接进入老年代
五、CMS Old GC频繁 ?
问题表现
- Old区频繁执行CMS GC
- GC日志中出现大量"CMS Initial Mark"和"CMS Final Remark"
- 每次回收效果不佳
底层原因
// CMS GC触发条件
void CMSCollector::shouldConcurrentCollect() {
// 判断是否需要触发CMS GC
if (_cmsGen->should_concurrent_collect()) {
// 1. 根据空间占用率判断
if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
return true;
}
// 2. 根据增长率判断
if (isGrowingTooFast()) {
return true;
}
}
}
主要原因:
- CMS启动阈值过高
- 空间碎片化严重
- 对象分配速率过快
- 浮动垃圾过多
解决方案
- 调整CMS触发阈值
// 降低触发阈值,提前启动GC
-XX:CMSInitiatingOccupancyFraction=68
-XX:+UseCMSInitiatingOccupancyOnly
- 增大Old区空间
-XX:CMSMaxAbortablePrecleanTime=5000
- 开启空间碎片整理
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
六、单次CMS Old GC耗时长
问题表现
- CMS GC单次停顿时间长
- 出现较长的STW时间
- Final Remark阶段耗时过长
底层原因
// CMS Final Remark阶段的核心代码
void CMSCollector::checkpointRootsFinalWork() {
// 1. 处理Reference
ReferenceProcessor::processReferences();
// 2. 类卸载
if (should_unload_classes()) {
ClassUnloadingWork();
}
// 3. 清理符号表
SymbolTable::clean_up();
}
耗时原因:
- Reference处理慢
- Class卸载耗时长
- Symbol Table清理慢
- 新生代状态不稳定
解决方案
- 优化Reference处理
// 开启并行Reference处理
-XX:+ParallelRefProcEnabled
- 控制Class卸载
// 关闭类卸载
-XX:-CMSClassUnloadingEnabled
- 稳定新生代状态
// Final Remark前强制Young GC
-XX:+CMSScavengeBeforeRemark
七、内存碎片&收集器退化
问题表现
- CMS退化为Serial Old收集器
- 出现"Promotion Failed"或"Concurrent Mode Failure"
- Full GC频繁发生
底层原因
// CMS并发失败处理
void ConcurrentMarkSweepGeneration::collect_in_background() {
if (shouldConcurrentCollect()) {
// 并发收集失败,退化为Serial收集
if (_foregroundGCIsActive) {
collectSerially();
return;
}
// 执行并发收集
collectConcurrently();
}
}
退化原因:
- 空间碎片化严重
- 并发模式失败
- 晋升失败
- 显式GC触发
解决方案
- 控制碎片率
// 配置碎片整理
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=10
- 调整并发参数
// 增加并发线程
-XX:ConcGCThreads=4
// 提前启动CMS
-XX:CMSInitiatingOccupancyFraction=68
- 预留足够空间
// 增大老年代空间
-Xmx4g -Xms4g
八、堆外内存OOM
问题表现
- 进程内存超出堆内存限制
- 出现Direct Memory OOM
- 堆外内存增长快速
底层原因
// DirectByteBuffer分配堆外内存
class DirectByteBuffer {
DirectByteBuffer(int cap) {
// 通过Unsafe分配本地内存
base = unsafe.allocateMemory(size);
// 通过Cleaner管理内存回收
cleaner = Cleaner.create(this, new Deallocator(base, size));
}
}
OOM原因:
- DirectByteBuffer使用不当
- 堆外内存泄漏
- JNI调用未释放
- 本地内存过大
解决方案
- 限制DirectMemory大小
-XX:MaxDirectMemorySize=256M
- 检查DirectByteBuffer使用
- 排查JNI调用
- 开启NMT监控
-XX:NativeMemoryTracking=detail
九、JNI引发的GC问题
问题表现
- GC Locker导致的GC
- Young GC停顿时间变长
- 对象过早晋升
底层原因
// GC Locker机制
class GCLocker {
static void lock() {
// 阻止GC发生
_needs_gc = true;
_jni_lock_count++;
}
static void unlock() {
_jni_lock_count--;
if (_jni_lock_count == 0 && _needs_gc) {
// 触发一次GC
Universe::heap()->collect();
}
}
}
问题原因:
- JNI临界区阻塞GC
- 触发额外的GC
- 导致对象提前晋升
解决方案
- 优化JNI代码
- 减少JNI调用时间
- 避免在JNI中长时间持有锁
- 添加GC参数
// 打印JNI引起的GC信息
-XX:+PrintJNIGCStalls
- 调整堆内存配置
// 增大新生代空间
-XX:NewSize=256m
-XX:MaxNewSize=256m
写在最后 ?
以上九种CMS GC问题是最常见的场景,关键是:
- 建立完善的监控体系
- 保留现场和日志信息
- 掌握问题分析方法
- 积累优化调优经验
记住一句话:
"GC优化不是目的,解决业务问题才是根本。"
#Java性能优化 #GC调优 #JVM