前言
我们在面试时,经常会被面试官问到:线上服务频繁Full GC该如何优化?
今天这篇文章跟大家一起聊聊这个话题,希望对你会有所帮助。
1. 什么是Full GC?
当老年代空间不足时,JVM会触发Stop-The-World的全局回收(Full GC),暂停所有应用线程。
致命危害(生产环境实测):
暂停时间业务影响1秒支付超时率上升5%3秒数据库连接池耗尽10秒服务被注册中心摘除对象的晋升之路流程图:
关键代码:年龄计数器- // HotSpot虚拟机源码片段(objectMonitor.cpp)
- void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock) {
- if (obj->age() >= MaxTenuringThreshold) { // 年龄阈值检查
- promote_to_old_gen(obj); // 晋升老年代
- }
- }
复制代码 2.如何排查定位问题?
2.1 实时监控:GC健康度速诊
- jstat -gcutil <pid> 1000 # 每秒输出GC数据
复制代码 关键指标解读:
- OU:老年代使用率 > 90% = 危险区
- FGCT:Full GC总耗时 > 应用运行时间10% = 严重问题
2.2. 堆内存转储:揪出内存黑洞
- jmap -dump:live,format=b,file=heap.bin <pid> # 生产环境慎用live
复制代码 2.3 MAT深度分析:解剖内存泄漏
3.优化方案
方案1:对象池化——大对象的救赎
场景:高频创建10MB的文件缓存- // 反例:每次请求创建新对象
- public void processRequest(Request req) {
- byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB
- // ...处理逻辑
- }
- // 优化:对象池复用
- private static final ObjectPool<byte[]> pool = new GenericObjectPool<>(
- new BasePooledObjectFactory<byte[]>() {
- @Override
- public byte[] create() {
- return new byte[10 * 1024 * 1024];
- }
- }
- );
- public void processRequest(Request req) throws Exception {
- byte[] buffer = pool.borrowObject();
- try {
- // ...处理逻辑
- } finally {
- pool.returnObject(buffer);
- }
- }
复制代码 效果:老年代分配速率下降85%
方案2:手动控制晋升
问题:Survivor区过小导致对象提前晋升
优化参数:- -XX:TargetSurvivorRatio=60 # Survivor区使用阈值
- -XX:MaxTenuringThreshold=15 # 最大晋升年龄
- -XX:+NeverTenure # 若Survivor足够,永不晋升(慎用!)
复制代码 晋升原理:
方案3:合理分配堆空间
经典误区:- -Xmx4g -Xms4g # 错误!未配置新生代
复制代码 优化公式:- 新生代大小 = 总堆 * 3/8
- Eden:Survivor = 8:1:1
复制代码 正确配置:- -Xmx8g -Xms8g
- -Xmn3g # 新生代3G (8*3/8≈3)
- -XX:SurvivorRatio=8 # Eden:Survivor=8:1:1
复制代码 方案4:卸载无用类
场景:热部署频繁的应用(如JRebel)
诊断命令:- jcmd <pid> VM.class_stats # JDK8+
- jcmd <pid> GC.class_stats # JDK11+
复制代码 根治代码:- // 自定义类加载器必须实现close()
- public class HotSwapClassLoader extends URLClassLoader {
- @Override
- public void close() throws IOException {
- // 1. 停止新请求
- // 2. 卸载所有类
- // 3. 关闭资源
- }
- }
复制代码 方案5:颠覆传统的ZGC
传统GC痛点:
- CMS:内存碎片问题
- G1:Mixed GC不可控
ZGC迁移步骤:
- -XX:+UseZGC
- -XX:ZAllocationSpikeTolerance=5.0 # 容忍内存分配速率波动
- -Xmx16g -Xlog:gc*:file=gc.log
复制代码 效果对比:
指标CMSZGCFull GC次数15次/天0次/天最大暂停2.8秒1.2毫秒方案6:堆外内存治理
现象:堆内存正常,但Full GC频繁
根源:DirectByteBuffer的清理依赖Full GC
防御方案:- // 方案1:限制堆外内存
- -XX:MaxDirectMemorySize=512m
- // 方案2:主动调用Cleaner
- ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
- if (cleaner != null) cleaner.clean();
- // 方案3:Netty的内存管理
- PooledByteBufAllocator allocator = new PooledByteBufAllocator(true);
- ByteBuf buffer = allocator.directBuffer(1024);
- // ...使用后必须release!
- buffer.release();
复制代码 4.实战案例
背景:某支付系统日均交易10亿
症状:
- 每分钟5次Full GC,暂停4.2秒
- 99线响应时间从50ms飙升至3秒
排查过程:
- jstat显示老年代10秒内从60%→99%
- MAT分析发现ConcurrentHashMap$Node[]占78%内存
- 溯源代码找到缓存黑洞:
- // 问题代码:永不失效的缓存
- Map<String, Transaction> cache = new ConcurrentHashMap<>();
- public void cacheTransaction(Transaction tx) {
- cache.put(tx.getId(), tx); // Key冲突时旧对象未移除!
- }
复制代码 解决方案:
- Cache<String, Transaction> cache = Caffeine.newBuilder()
- .maximumSize(10_000)
- .expireAfterWrite(5, TimeUnit.MINUTES)
- .build();
复制代码- // 用有界队列替代LinkedBlockingQueue
- new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000));
复制代码 效果:
总结
- jstat -gcutil <pid> 1000 # 实时监控
- -Xlog:gc*:file=gc.log # GC日志
- Prometheus + Grafana # 可视化大盘
复制代码
<ol start="3">代码军规:
- 大对象必须池化
- 缓存必须设置上限
- 线程池必须用有界队列
GC算法选择:[table][tr]场景推荐算法[/tr][tr][td]堆 |