JVM深度调优:内存泄漏排查与GC算法优化实战(真实踩坑记录)

avatar
莫雨IP属地:上海
02026-02-18:22:03:25字数 3418阅读 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=200Full 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:目标停顿<200ms
  • InitiatingHeapOccupancyPercent=35:堆占用35%时触发GC,避免突然Full GC

3. 优化前后对比

指标优化前优化后提升
平均响应时间1500ms250ms83%↓
GC停顿时间(99%分位)800ms150ms81%↓
Full GC次数/天120次3次97%↓

三、避坑指南(全是血的教训)

  1. 别信“调大-Xmx就行”

    • 问题:有人把-Xmx从4G调到8G,结果GC停顿更长。
    • 原因:JVM会尝试分配更大内存,导致GC时间指数级增长。
    • 正确做法:先修复内存泄漏,再调GC参数。
  2. GC日志必须开,别偷懒

    • 问题:线上没开GC日志,排查时只能靠猜。
    • 工具推荐:用gcviewer(开源)分析日志,比看命令行直观10倍。
  3. 别乱用ZGC(新手慎用)

    • 问题:ZGC在Java 11+才稳定,我们升级到11时,发现它对小内存应用(<4G)反而更卡。
    • 建议:小项目用G1,大项目(>16G)再上ZGC。
  4. 监控要覆盖到JVM

    • 必须监控:
      • jvm.memory.used(堆内存使用率)
      • jvm.gc.pause.time(GC停顿时间)
    • 我们用Prometheus + Grafana,设置告警:jvm_gc_pause_time_seconds_sum > 0.5(停顿>500ms告警)。

四、真实案例:我们怎么做到的

场景:订单服务,峰值QPS 5000,要求响应<300ms。
优化步骤

  1. 用MAT定位缓存泄漏 → 修复代码(1小时)。
  2. 开GC日志 → 分析发现G1停顿长 → 调整MaxGCPauseMillis=200(10分钟)。
  3. 用Prometheus监控 → 设置停顿告警(1小时)。

结果

  • 30天无OOM,响应时间稳定在200ms内。
  • 服务器从4台缩到3台(节省成本)。

结语:JVM不是玄学,是工具

别再被“JVM调优很难”吓到。我的经验就三点:

  1. 先治标(找内存泄漏)→ 用MAT抓堆转储。
  2. 再治本(调GC)→ 根据场景选算法,别乱改参数。
  3. 全程监控→ GC日志+Prometheus,别靠感觉。

去年我们被JVM坑了3次,这次我写完直接发到团队Wiki。现在运维同事说:“这比看文档强多了。”
现在就做

  • 开GC日志(-Xloggc:...
  • 用MAT分析一次堆转储
  • 调整G1参数试试

别等OOM了再后悔。

总资产 0
暂无其他文章

热门文章

暂无热门文章