一、FullGC为什么让人头疼
做Java开发的同学应该都遇到过这样的场景:系统运行一段时间后,响应越来越慢,监控一看,FullGC频繁得跟闹钟似的。这时候老板的脸色可能比FullGC的STW(Stop-The-World)还要难看。
FullGC频繁本质上是因为老年代空间被快速填满,导致JVM不得不停下所有应用线程来清理垃圾。想象一下,你家的垃圾桶总是很快就满了,不得不频繁下楼倒垃圾,这期间啥家务都做不了——FullGC就是这么个烦人的过程。
示例场景:电商促销导致FullGC暴增
假设我们有个Java开发的秒杀系统,平时运行良好,但大促时频繁FullGC:
// 技术栈:Java 8 + SpringBoot
@RestController
public class SeckillController {
// 使用HashMap缓存秒杀商品(问题代码!)
private static Map<Long, Item> cache = new HashMap<>();
@PostMapping("/seckill")
public Result seckill(@RequestBody Order order) {
// 1. 检查库存(从缓存读取)
Item item = cache.get(order.getItemId());
if (item.getStock() <= 0) {
return Result.fail("已售罄");
}
// 2. 扣减库存(未考虑并发问题)
item.setStock(item.getStock() - 1);
// 3. 生成订单(模拟老年代对象积累)
OrderService.createOrder(order); // 每次调用产生200KB订单对象
return Result.success();
}
}
问题分析:
HashMap缓存没有设置大小限制,大促时可能堆积GB级数据createOrder产生的订单对象最终会进入老年代- 没有处理并发场景,导致产生多余对象
二、揪出FullGC的元凶
2.1 工具准备
工欲善其事,必先利其器:
jstat -gcutil [pid] 1000:实时查看GC统计jmap -histo:live [pid]:查看对象分布- GC日志(添加JVM参数):
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log
2.2 实战分析
通过GC日志发现这样的记录:
2023-08-20T14:30:01.123+0800: [Full GC (Allocation Failure)
[PSOldGen: 819200K->819199K(819200K)]
1024000K->1023999K(1024000K),
[Metaspace: 25600K->25600K(1056768K)]
关键指标:
- 老年代(PSOldGen)回收前后几乎无变化
- 回收后空间仍然不足(Allocation Failure)
三、调优三十六计
3.1 基础参数调整
# 建议配置(针对8核机器,堆内存4G)
-Xms4g -Xmx4g # 避免堆动态扩容
-XX:NewRatio=2 # 新生代:老年代=1:2
-XX:SurvivorRatio=8 # Eden:Survivor=8:1:1
-XX:+UseConcMarkSweepGC # CMS收集器(Java8推荐)
3.2 代码优化方案
改造之前的秒杀代码:
// 优化版本:使用Caffeine缓存 + 对象复用
@RestController
public class SeckillControllerV2 {
// 使用带上限的缓存
private static Cache<Long, Item> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
// 对象池复用订单对象
private static ObjectPool<Order> orderPool = new GenericObjectPool<>(
new BasePooledObjectFactory<Order>() {
@Override public Order create() { return new Order(); }
@Override public void passivateObject(Order obj) { obj.clear(); }
}
);
@PostMapping("/v2/seckill")
public Result seckill(@RequestBody Order order) throws Exception {
Item item = cache.get(order.getItemId(), k -> queryFromDB(k));
// 使用CAS保证原子性
while (true) {
int stock = item.getStock();
if (stock <= 0) return Result.fail("已售罄");
if (item.cas(stock, stock - 1)) break;
}
// 从对象池获取订单对象
Order pooledOrder = orderPool.borrowObject();
pooledOrder.copyFrom(order);
OrderService.createOrder(pooledOrder);
orderPool.returnObject(pooledOrder);
return Result.success();
}
}
优化点:
- 用Caffeine替代HashMap,避免内存无限增长
- 引入对象池减少老年代对象创建
- 采用CAS解决并发问题
四、防患于未然的建议
4.1 监控体系建设
推荐配置:
- Prometheus + Grafana监控JVM指标
- 关键报警项:
- FullGC次数 > 1次/分钟
- 老年代使用率 > 80%持续5分钟
4.2 压测验证方法
使用JMeter模拟大促流量:
<!-- JMeter测试片段 -->
<ThreadGroup>
<duration>300</duration> <!-- 持续5分钟 -->
<rate>1000</rate> <!-- 1000请求/秒 -->
<httpSampler>
<path>/v2/seckill</path>
<postBody>{"itemId":123}</postBody>
</httpSampler>
</ThreadGroup>
4.3 其他注意事项
- 谨慎使用大对象(如大数组),容易直接进入老年代
- 避免在循环中创建集合对象
- 注意第三方库的内存泄漏(如未关闭的连接池)
五、总结回顾
FullGC频繁的本质是内存管理失衡。通过本文的案例我们可以看到,从参数调整、代码优化到监控预警,每个环节都需要精心设计。记住一个黄金法则:让对象尽可能在年轻代消亡,减少晋升到老年代的机会。
当遇到FullGC问题时,建议按照这个流程排查:
- 确认现象(通过GC日志)
- 定位问题对象(jmap/jvisualvm)
- 针对性优化(代码/参数)
- 验证效果(压测+监控)
评论