java内存泄漏表现为老年代内存单向增长且full gc释放极少,需用jstat监控ogc/ou、jmap抓堆转储、mat分析leak suspects或dominator tree定位静态集合、threadlocal、监听器未注销等典型问题。

Java内存泄漏在长期运行程序中往往不表现为立即崩溃,而是表现为缓慢、持续的堆内存增长,尤其在老年代(Old Gen)中难以通过Full GC释放。这种“温水煮青蛙”式的问题最难察觉,却最易拖垮服务稳定性。
观察GC行为与内存趋势
先确认是否真有泄漏,而不是单纯高负载导致的内存占用上升:
- 用 jstat -gc
5000 每5秒查看一次GC统计,重点关注 OGCMN/OGCMX(老年代初始/最大值)、OGC(当前老年代容量) 和 OU(老年代已用) —— 若 OU 随时间单向爬升、Full GC 后下降幅度极小(如仅降1%~2%),高度可疑 - 配合 jstat -gccause
5000 - 观察 Metaspace 是否同步增长,排除类加载器泄漏(如热部署、OSGi、动态字节码生成未清理)
抓取并分析堆转储(Heap Dump)
在内存使用达60%~70%且趋势稳定上升时触发快照,避免OOM后dump失败:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 执行 jmap -dump:format=b,file=heap.hprof
获取二进制dump文件 - 用 Eclipse MAT 打开,优先点开 Leak Suspects Report —— 它会自动标记出最可能的泄漏源头(如某个静态Map占了80% Retained Heap)
- 若报告无明确提示,切换到 Dominator Tree,按 Retained Heap 降序排列,聚焦前3~5个大对象;右键 → Path to GC Roots → exclude weak/soft references,查看强引用链
- 特别注意:java.lang.Thread 实例数异常增多(线程泄漏)、org.springframework.context.support.GenericApplicationContext 多个实例(上下文未关闭)、或自定义缓存类持有大量业务对象
定位典型泄漏模式
长期运行程序中最常踩的坑集中在生命周期错配:
-
静态集合未清理:如
private static final Map<string object> CACHE = new HashMap();</string>不设过期、不加size限制、不提供清除入口 -
ThreadLocal未remove:在线程池场景下,线程复用导致value长期滞留;必须在业务逻辑末尾调用
threadLocal.remove(),不能只设为null -
监听器/回调未注销:注册了事件监听但销毁时没反向调用
removeListener(),尤其在Spring Bean作用域为prototype或手动管理生命周期时 -
内部类持外部类引用:非静态内部类默认持有
this引用;若该内部类被静态变量、线程或缓存持有,整个外部类实例无法回收
验证修复与持续监控
改完代码不能只靠“感觉”,要闭环验证:
- 在测试环境模拟长时间运行(至少2~3轮Full GC周期),用相同jstat命令对比OU变化斜率是否归零或显著放缓
- 加入JVM启动参数 -XX:+PrintGCDetails -Xloggc:gc.log,分析GC日志中 OldGen occupancy 的波动规律
- 对关键模块启用 WeakReference 或 SoftReference 包装缓存对象,降低泄漏风险;必要时用 ConcurrentHashMap 替代 static HashMap 并配合 computeIfAbsent + 定时清理线程
- 上线后接入APM工具(如SkyWalking、Arthas)配置内存告警阈值,设置 Old Gen 使用率 > 75% 持续5分钟 的通知机制
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










