大家好,我是苏三,又跟大家见面了。
前言
我们在面试时,经常会被面试官问到:线上服务频繁Full GC该如何优化?
今天这篇文章跟大家一起聊聊这个话题,希望对你会有所帮助。
最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。
扫码加苏三的微信:li_su223,备注:所在城市,即可进群。
- 什么是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 = newbyte[10 * 1024 * 1024]; // 10MB
// ...处理逻辑
}
// 优化:对象池复用
privatestaticfinal ObjectPool<byte[]> pool = new GenericObjectPool<>(
new BasePooledObjectFactory<byte[]>() {
@Override
publicbyte[] create() {
returnnewbyte[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迁移步骤 :
- 升级JDK至17+
- 添加参数:
-XX:+UseZGC
-XX:ZAllocationSpikeTolerance=5.0 # 容忍内存分配速率波动
-Xmx16g -Xlog:gc*:file=gc.log
效果对比 :
| 指标 | CMS | ZGC | | --- | --- | --- | | Full 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冲突时旧对象未移除!
}
解决方案 :
- 改用Caffeine缓存:
Cache<String, Transaction> cache = Caffeine.newBuilder()
.maximumSize(10\_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
- 添加ZGC参数
- 重写线程池任务队列:
// 用有界队列替代LinkedBlockingQueue
new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000));
效果 :
- Full GC降为0
- 99线回落至68ms
总结
- 监控三件套 :
jstat -gcutil <pid> 1000 # 实时监控
-Xlog:gc*:file=gc.log # GC日志
Prometheus + Grafana # 可视化大盘
- 参数黄金法则 :
- 代码军规 :
- 大对象必须池化
- 缓存必须设置上限
- 线程池必须用有界队列
- GC算法选择 :
| 场景 | 推荐算法 | | --- | --- | | 堆<8G | Parallel | | 8G~32G | G1 | | 关键业务系统 | ZGC |
Full GC不是优化出来的,是设计出来的!
永远在架构设计阶段预留30%内存缓冲空间 ,比任何调参技巧都重要。
附录:急救工具箱
|
工具
|
命令
|
适用场景
|
| --- | --- | --- |
|
jcmd
| jcmd <pid> GC.run
|
主动触发Full GC
|
|
Arthas
| vmtool --action getHeap
|
内存快照
|
|
btrace
|
监控DirectByteBuffer分配
|
堆外内存泄漏
|
|
PerfMa
|
在线分析GC日志
|
自动化诊断
|
最后欢迎加入苏三的星球,你将获得:100万QPS短链系统、复杂的商城微服务系统、苏三AI项目、刷题吧小程序、秒杀系统、商城系统、秒杀系统、代码生成工具等8个项目的源代码、开发教程和技术答疑。
系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。
还有1V1免费修改简历、技术答疑、职业规划、送书活动、技术交流。
扫描下方二维码,可以优惠30元:
只有20张优惠券,数量有限,先到先得。
目前星球已经更新了5800+篇优质内容,还在持续爆肝中.....