JVM 内存泄漏(Memory Leak)内存溢出(Memory Overflow/OOM)

JVM

内存泄漏(Memory Leak)和内存溢出(Memory Overflow/OOM)是两类不同的内存问题,其根本原因和解决方案有显著区别

内存泄漏(Memory Leak)

问题本质 : 对象已不再被使用,但因错误引用无法被 GC 回收,导致内存被持续占用,最终可能引发 OOM

核心解决思路定位并切断无效引用链

解决方案

1.定位泄漏点(关键步骤)

工具

  • jmap + MAT(Memory Analyzer Tool):生成堆转储(Heap Dump),分析对象引用链。
  • VisualVM:实时监控堆内存,跟踪对象增长趋势。
  • Arthas :在线诊断,执行 heapdump 命令动态生成 Dump。

操作

  • 对比多次 Heap Dump,找到 持续增长且未被回收的对象

  • 在 MAT 中查看 GC Roots 引用链,找到意外持有对象的根源(如静态集合、线程池)

2.修复代码:切断无效引用

2.1.静态集合类 :使用 WeakHashMap 或定期清理(如 remove()

java 复制代码
     // 错误示例:静态List持有对象
     private static List<Object> cache = new ArrayList<>();
     // 修复:改用弱引用或添加清理逻辑
     private static Map<Object, WeakReference<Object>> weakCache = new WeakHashMap<>();

2.2.监听器/回调 :及时注销(如 removeEventListener()
2.3.资源对象 :确保 close()(用 try-with-resources 语法)

java 复制代码
     // 正确关闭资源
     try (InputStream is = new FileInputStream("file.txt")) {
         // 使用资源
     } // 自动调用 is.close()

2.4.避免生命周期错配

非静态内部类隐式持有外部类 → 改为 静态内部类

3.预防措施

  • 代码规范:避免在长生命周期对象(如静态集合)中持有短生命周期对象。
  • 代码审查:重点关注集合操作、资源关闭、监听器注册。
  • 自动化测试:用 LeakCanary(Android)JMH + 内存分析 检测泄漏。

内存溢出(OOM)

问题本质 : JVM 堆内存不足非堆内存耗尽,无法分配新对象。

核心解决思路扩容内存减少内存消耗

解决方案

1. 扩容堆内存

  • 调整 JVM 参数(临时方案):
bash 复制代码
     -Xms4g -Xmx4g   # 初始堆=最大堆(避免动态扩容开销)
     -XX:MaxMetaspaceSize=512m  # 限制元空间大小
  • 风险 :单次 GC 时间变长(STW 暂停时间增加):单次 GC 时间变长是指在应用程序运行过程中,垃圾回收(Garbage Collection,简称GC)所消耗的时间相比于之前变得更长,意思就是回收效率越来越慢

2. 优化内存使用

  • 减少对象创建
    • 重用对象:使用 对象池(如 Apache Commons Pool)。
    • 避免创建大对象:如大数组 → 改用分块处理。
  • 调整数据结构
    • ArrayList 替代 LinkedList(内存更紧凑)。
    • 原始类型集合:用 FastUtilEclipse Collections 避免装箱。
  • 缓存优化
    • 限制缓存大小:LRU 策略(如 Guava CachemaximumSize())。
    • 软引用/弱引用缓存(SoftReference 在内存不足时自动释放)。

3. 优化 GC 策略

  • 选择低停顿收集器(如 G1ZGC):
bash 复制代码
     -XX:+UseG1GC   # G1 收集器(JDK9+ 默认)
     -XX:+UseZGC    # ZGC(JDK15+ 生产可用)
  • 调整分代比例(针对分代收集器):
bash 复制代码
     -XX:NewRatio=2          # 老年代:新生代=2:1
     -XX:SurvivorRatio=8     # Eden:Survivor=8:1:1

4. 处理非堆 OOM

  • Metaspace OOM
    • 原因:加载过多类(如动态生成类、重复加载)。
    • 解决:
      • 增加元空间:-XX:MaxMetaspaceSize=256m
      • 排查类加载泄漏:用 jcmd <pid> VM.classloader_stats 查看类加载器。
  • 栈溢出(StackOverflowError)
    • 原因:递归过深或循环调用。
    • 解决:优化递归为循环;增大栈空间 -Xss2m(谨慎使用)

5. 兜底方案

  • 捕获 OOM 并优雅降级:
java 复制代码
     try {
         // 可能触发 OOM 的操作
     } catch (OutOfMemoryError e) {
         System.gc();          // 尝试紧急回收(不保证生效)
         fallbackOperation();  // 执行降级逻辑(如清理缓存、返回默认值)
     }
  • 自动 Dump 内存快照:
bash 复制代码
     -XX:+HeapDumpOnOutOfMemoryError 
     -XX:HeapDumpPath=/path/to/dump.hprof

问题对比与综合预防

维度 内存泄漏 内存溢出
根本原因 对象无法回收(引用未释放) 内存需求超过 JVM 上限
解决重点 定位无效引用链 + 修复代码 扩容内存 + 减少消耗 + GC 调优
工具 MAT、VisualVM、Arthas JVM 参数、GC 日志分析器(GCeasy)
预防手段 代码规范、LeakCanary、代码审查 容量规划、压测、限流、降级策略
紧急恢复 重启应用(临时) 扩容实例、重启 + 内存参数调整

通用最佳实践

1. 监控预警:

  • 使用 Prometheus + Grafana 监控堆内存、GC 次数、Metaspace 使用率。

2. 压测验证:

  • 通过 JMeter/Siege 模拟流量,观察内存增长是否稳定。

3. 代码层面:

  • 避免 static 滥用,及时解引用
  • 使用 -Xlint:unchecked 编译选项检测集合操作警告。

4. JVM 调优:

  • 定期分析 GC 日志(-Xlog:gc*):
bash 复制代码
     java -Xlog:gc*:file=gc.log -jar app.jar
  • 使用 GCeasy 等工具自动化分析 GC 日志

关键总结

  • 内存泄漏是程序 Bug,必须修复代码
  • 内存溢出是资源问题,需结合扩容和优化
  • 二者可能相互转化:长期泄漏必导致 OOM,而 OOM 可能暴露泄漏点。
    通过 监控 + 分析工具 + 代码规范 可预防 90% 的内存问题。