深度解析Java中内存溢出(OOM)的典型案例与避坑指南

 更新时间:2026年04月08日 09:44:33   作者:无心水  
内存溢出(OOM)依然是生产环境中常见的隐形杀手,本文通过三个真实案例,揭示业务代码中容易忽略的OOM场景,并给出解决方案,助你从根本上避免类似问题

自动垃圾收集器不是万能的,这些隐蔽的OOM陷阱你遇到过吗?

Java的自动垃圾收集器(GC)让我们能专注于业务逻辑,但内存溢出(OOM)依然是生产环境中常见的“隐形杀手”。本文通过三个真实案例,揭示业务代码中容易忽略的OOM场景,并给出解决方案。我们将使用关键代码和Mermaid图直观展示问题本质,帮助你从根本上避免类似问题。

案例一:太多份相同的对象导致OOM

场景描述

某项目需要实现用户名的自动补全功能(类似搜索框的联想提示)。开发同学设计了一个内存缓存:以用户名的所有前缀作为Key,Value是对应前缀的用户列表。例如用户“aa”和“ab”会生成Key“a”、“aa”、“ab”,输入“a”时就能返回两个用户。

问题代码

private ConcurrentHashMap<String, List<UserDTO>> autoCompleteIndex = new ConcurrentHashMap<>();

@PostConstruct
public void wrong() {
    // 从数据库加载所有用户(假设1万个)
    userRepository.findAll().forEach(userEntity -> {
        int len = userEntity.getName().length();
        // 为每个用户名的前1~N位创建索引
        for (int i = 0; i < len; i++) {
            String key = userEntity.getName().substring(0, i + 1);
            autoCompleteIndex.computeIfAbsent(key, s -> new ArrayList<>())
                    .add(new UserDTO(userEntity.getName())); // 每次都new对象
        }
    });
}

每个UserDTO除了用户名还包含10KB的模拟数据。运行后,1万个用户却生成了6万个UserDTO对象,占用约1.2GB内存,远超预期。

问题分析

虽然只有1万个真实用户,但每个用户名平均长度6位,因此产生了6万个索引条目,每个条目都创建了新的UserDTO对象,导致内存中对象数量膨胀6倍。

原始方案的对象关系图:

解决方案

使用HashSet去重,确保每个用户只保留一份UserDTO,所有索引Key的List都引用同一份对象。

@PostConstruct
public void right() {
    // 先构建去重的用户缓存
    HashSet<UserDTO> cache = userRepository.findAll().stream()
            .map(item -> new UserDTO(item.getName()))
            .collect(Collectors.toCollection(HashSet::new));

    // 构建索引时共享对象
    cache.forEach(userDTO -> {
        int len = userDTO.getName().length();
        for (int i = 0; i < len; i++) {
            String key = userDTO.getName().substring(0, i + 1);
            autoCompleteIndex.computeIfAbsent(key, s -> new ArrayList<>())
                    .add(userDTO); // 共享同一对象
        }
    });
}

优化后的对象关系图:

优化后,内存占用降至不足200MB,对象数量从6万减至约1万。

教训

  • 容量评估时不能想当然地认为“一份数据在内存中也是一份”。经过框架转换、多次复制,内存占用可能成倍增长。
  • 使用集合缓存时,务必考虑对象复用,避免重复创建。

案例二:使用WeakHashMap不等于不会OOM

场景描述

开发者想用WeakHashMap作为缓存,认为当Key不再被外部引用时,Entry会自动被GC回收,避免内存堆积。于是实现了如下代码,缓存200万个用户资料。

private Map<User, UserProfile> cache = new WeakHashMap<>();

@GetMapping("wrong")
public void wrong() {
    String userName = "zhuye";
    LongStream.rangeClosed(1, 2000000).forEach(i -> {
        User user = new User(userName + i);
        cache.put(user, new UserProfile(user, "location" + i));
    });
}

运行后却发现cache.size()始终是200万,最终导致OOM。

问题分析

WeakHashMap的Key是弱引用,但ValueUserProfile却持有User对象的强引用(通过其user字段)。这导致即使外部的user变量不再使用,User对象仍然被UserProfile引用,无法被GC回收。

引用关系图(问题版):

当GC发生时,Key(User)只有弱引用,本应被回收,但因Value中的强引用,整个Entry无法从ReferenceQueue中移除,导致内存泄漏。

解决方案

Value也使用弱引用包装,切断Value对Key的强引用链。

private Map<User, WeakReference<UserProfile>> cache2 = new WeakHashMap<>();

@GetMapping("right")
public void right() {
    String userName = "zhuye";
    LongStream.rangeClosed(1, 2000000).forEach(i -> {
        User user = new User(userName + i);
        cache2.put(user, new WeakReference<>(new UserProfile(user, "location" + i)));
    });
}

或者重新创建User对象,使UserProfile不再引用原来的Key:

cache.put(user, new UserProfile(new User(user.getName()), "location" + i));

优化后的引用关系:

现在,当Key只被弱引用时,GC可以回收Key,同时WeakReference<UserProfile>也会被回收,Entry最终被清除。

补充

Spring提供的ConcurrentReferenceHashMap支持Key和Value同时使用软引用或弱引用,线程安全且性能更好,是更优的选择。

案例三:Tomcat参数配置不合理导致OOM

场景描述

某应用在业务高峰期频繁出现OOM,堆Dump显示有大量1.7GB的byte数组,占满了2GB的堆内存。分析发现,这些数组来自Tomcat的工作线程。

问题代码

查看项目配置,发现有人修改了Tomcat的max-http-header-size参数:

server.max-http-header-size=10000000

起因是开发遇到了java.lang.IllegalArgumentException: Request header is too large异常,搜索后简单地将该参数改为一个超大值(10MB),期望永远不再报错。

问题分析

Tomcat的Http11InputBufferHttp11OutputBuffer会根据max-http-header-size分配固定大小的缓冲区。该配置导致每个请求的Request和Response各占用约10MB内存(实际InputBuffer稍大)。假设有100个工作线程,仅缓冲区就占用近2GB,加上业务对象,很容易OOM。

请求处理内存分配示意图:

解决方案

将参数改为合理值,例如20000(20KB),并压测验证:

server.max-http-header-size=20000

教训

  • 修改参数前要理解其含义,容量类参数背后往往代表资源占用,不能随意设置超大值。
  • 建议预留2~5倍余量,但必须结合实际需求。

总结与建议

1.对象复用意识:相同数据可能因多次转换、索引等原因在内存中存在多份,使用HashSet等去重可大幅降低内存占用。

2.引用类型陷阱WeakHashMap的Value若持有Key的强引用,会导致Key无法回收。使用弱引用包装Value或切断引用链。

3.合理配置资源:Tomcat等中间件的容量参数需谨慎设置,过大会直接导致内存暴涨。

4.OOM排查手段

启用GC日志和HeapDumpOnOutOfMemoryError:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

使用MAT、jvisualvm等工具分析堆Dump,定位大对象和引用链。

思考与讨论

  • Spring的ConcurrentReferenceHashMap支持Key和Value使用软引用或弱引用。你觉得哪种方式更适合做缓存?为什么?
  • 动态执行Groovy脚本时,每次new GroovyShell()会生成大量类,容易导致Metaspace OOM。你知道如何避免吗?

到此这篇关于深度解析Java中内存溢出(OOM)的典型案例与避坑指南的文章就介绍到这了,更多相关Java内存溢出避坑内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深度剖析java动态静态代理原理源码

    深度剖析java动态静态代理原理源码

    这篇文章主要介绍了深度剖析java动态静态代理原理源码,关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理。,需要的朋友可以参考下
    2019-06-06
  • 详解Java中数组判断元素存在几种方式比较

    详解Java中数组判断元素存在几种方式比较

    这篇文章主要介绍了Java中数组判断元素存在几种方式比较,非常不错,具有一定的参考借鉴价值,需要的朋友参考下吧
    2018-07-07
  • Java hutool List集合对象拷贝示例代码

    Java hutool List集合对象拷贝示例代码

    这篇文章主要介绍了Java hutool List集合对象拷贝的相关资料,文章还分享了在实现过程中遇到的一些问题,并强调了阅读源码和正确配置CopyOptions的重要性,需要的朋友可以参考下
    2024-12-12
  • ssm 使用token校验登录的实现

    ssm 使用token校验登录的实现

    这篇文章主要介绍了ssm 使用token校验登录的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Java算法之归并排序举例详解

    Java算法之归并排序举例详解

    这篇文章主要介绍了Java算法之归并排序的相关资料,归并排序是一种递归排序算法,通过将数组分成更小的子数组,递归地排序这些子数组,然后将它们合并成有序数组,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-04-04
  • Java Properties作为集合三个方法详解

    Java Properties作为集合三个方法详解

    Properties是JDK1.0中引入的java类,目前也在项目中大量使用,主要用来读取外部的配置,那除了这个,你对它其他的一些api也了解吗? 你了解它是怎么实现的吗? 如果不清楚的话,就通过本篇文章带你一探究竟
    2022-11-11
  • SpringBoot Redis配置Fastjson进行序列化和反序列化实现

    SpringBoot Redis配置Fastjson进行序列化和反序列化实现

    这篇文章主要介绍了SpringBoot Redis配置Fastjson进行序列化和反序列化实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • JAVA IDEA项目打包为jar包的步骤详解

    JAVA IDEA项目打包为jar包的步骤详解

    在Java开发中我们通常会将我们的项目打包成可执行的Jar包,以便于在其他环境中部署和运行,下面这篇文章主要给大家介绍了关于JAVA IDEA项目打包为jar包的相关资料,需要的朋友可以参考下
    2024-08-08
  • SpringMVC如何配置JSP视图解析器

    SpringMVC如何配置JSP视图解析器

    这篇文章主要介绍了SpringMVC如何配置JSP视图解析器问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • 一文搞懂SpringMVC中@InitBinder注解的使用

    一文搞懂SpringMVC中@InitBinder注解的使用

    @InitBinder方法可以注册控制器特定的java.bean.PropertyEditor或Spring Converter和 Formatter组件。本文通过示例为大家详细讲讲@InitBinder注解的使用,需要的可以参考一下
    2022-06-06

最新评论