0赞
赏
赞赏
更多好文
上周三凌晨3点,我被监控告警电话吵醒——线上订单服务突然OOM,整个集群挂了。老板在群里吼:“再出问题我让你去机房睡!” 这不是第一次了。去年我们被JVM坑惨过,这次我决定死磕到底,把内存泄漏和GC优化搞透。下面全是血泪经验,没一句废话。
一、内存泄漏排查:从“手忙脚乱”到“精准定位”
问题现象:
服务运行24小时后,内存从800M涨到2.5G,GC频率从每分钟1次飙到10次,最终OOM。
1. 第一步:抓现场(别等!)
别像我第一次那样等“明天再看”。直接上命令:
# 查看实时内存和GC状态
jstat -gcutil <PID> 5000 # 每5秒输出一次
# 生成堆转储(关键!)
jmap -dump:format=b,file=heap.hprof <PID>
重点:在OOM前立刻抓,不然JVM会直接退出。
2. 用Eclipse MAT分析堆转储
- 用MAT打开
heap.hprof,点“Leak Suspects”。 - 发现:
com.example.cache.UserCache对象堆积了12万条,内存占用80%。 - 代码定位:
// 问题代码:缓存没清理逻辑 public class UserCache { private static Map<Long, User> cache = new HashMap<>(); // 没用WeakReference public static void put(Long id, User user) { cache.put(id, user); // 永远不删! } }
血泪教训:
缓存是内存泄漏的重灾区!别用HashMap存业务对象,除非你确定能清理。
3. 修复方案(简单粗暴)
// 修复:用WeakHashMap+定时清理
public class UserCache {
private static Map<Long, User> cache = new WeakHashMap<>(); // 用WeakReference
// 每5分钟清理一次
public static void clean() {
cache.keySet().removeIf(id -> System.currentTimeMillis() - lastAccessTime.get(id) > 300000);
}
}
效果:
内存从2.5G稳定在800M,GC频率从10次/分钟降到1次/分钟。
二、GC算法优化:从“卡顿”到“丝滑”
问题现象:
修复泄漏后,服务响应时间从200ms飙升到1.5秒,用户投诉“卡得像PPT”。
1. 先看GC日志(必须开!)
在启动参数加:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/logs/gc.log
关键指标:
Full GC频率:频繁Full GC = 服务卡顿Pause Time:单次GC停顿>200ms = 用户能感知
2. 针对性调优(我的实战配置)
| 场景 | 问题 | 优化方案 | 效果 |
|---|---|---|---|
| 大内存应用(>16G) | Full GC太频繁 | -XX:+UseG1GC -XX:MaxGCPauseMillis=200 | Full GC从10次/小时→2次/小时 |
| 低延迟要求(<200ms) | GC停顿长 | -XX:+UseZGC -XX:ZCollectionInterval=10 | 平均停顿<5ms |
| 传统中小应用(<8G) | CMS碎片化 | -XX:+UseParallelGC | 停顿时间减少60% |
我的选择:
用G1 GC(Java 8+默认,但需显式配置):
# 生产环境启动参数
java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=35 -jar app.jar
为什么选G1?
- 适合大内存(我们用4G堆)
MaxGCPauseMillis=200:目标停顿<200msInitiatingHeapOccupancyPercent=35:堆占用35%时触发GC,避免突然Full GC
3. 优化前后对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 平均响应时间 | 1500ms | 250ms | 83%↓ |
| GC停顿时间(99%分位) | 800ms | 150ms | 81%↓ |
| Full GC次数/天 | 120次 | 3次 | 97%↓ |
三、避坑指南(全是血的教训)
-
别信“调大-Xmx就行”
- 问题:有人把
-Xmx从4G调到8G,结果GC停顿更长。 - 原因:JVM会尝试分配更大内存,导致GC时间指数级增长。
- 正确做法:先修复内存泄漏,再调GC参数。
- 问题:有人把
-
GC日志必须开,别偷懒
- 问题:线上没开GC日志,排查时只能靠猜。
- 工具推荐:用
gcviewer(开源)分析日志,比看命令行直观10倍。
-
别乱用ZGC(新手慎用)
- 问题:ZGC在Java 11+才稳定,我们升级到11时,发现它对小内存应用(<4G)反而更卡。
- 建议:小项目用G1,大项目(>16G)再上ZGC。
-
监控要覆盖到JVM
- 必须监控:
jvm.memory.used(堆内存使用率)jvm.gc.pause.time(GC停顿时间)
- 我们用Prometheus + Grafana,设置告警:
jvm_gc_pause_time_seconds_sum > 0.5(停顿>500ms告警)。
- 必须监控:
四、真实案例:我们怎么做到的
场景:订单服务,峰值QPS 5000,要求响应<300ms。
优化步骤:
- 用MAT定位缓存泄漏 → 修复代码(1小时)。
- 开GC日志 → 分析发现G1停顿长 → 调整
MaxGCPauseMillis=200(10分钟)。 - 用Prometheus监控 → 设置停顿告警(1小时)。
结果:
- 30天无OOM,响应时间稳定在200ms内。
- 服务器从4台缩到3台(节省成本)。
结语:JVM不是玄学,是工具
别再被“JVM调优很难”吓到。我的经验就三点:
- 先治标(找内存泄漏)→ 用MAT抓堆转储。
- 再治本(调GC)→ 根据场景选算法,别乱改参数。
- 全程监控→ GC日志+Prometheus,别靠感觉。
去年我们被JVM坑了3次,这次我写完直接发到团队Wiki。现在运维同事说:“这比看文档强多了。”
现在就做:
- 开GC日志(
-Xloggc:...) - 用MAT分析一次堆转储
- 调整G1参数试试
别等OOM了再后悔。
