有 GC为什么还会 OOM这么问好像略显白痴一些一句话答案GC 只能回收没人用的对象。如果对象一直有人拿着引用不放GC 永远不敢动它内存就会撑爆。二、用生活场景理解把 JVM 堆内存想象成一个停车场GC 是停车场管理员。停车场Heap 堆内存 ┌─────────────────────────────────────┐ │ 车A有人在用 │ │ 车B有人在用 │ │ 车C没人用但钥匙还插着 │ ← GC 不敢拖走 │ 车D有人在用 │ │ 车E有人在用 │ │ 车F有人在用 │ │ 车G有人在用 │ │ ....停满了 │ └─────────────────────────────────────┘GC 的工作原则只要有一把钥匙引用指向这辆车我就不能拖走它。OOM 发生的原因停车场停满了新车进不来但所有车都有钥匙管理员一辆都不能拖走。停车场满了新车来了 →java.lang.OutOfMemoryError: Java heap space三、那 GC 到底在干什么GC 会定期扫描把真正没人用的对象清掉GC 扫描 main() ─────▶ List list ─────▶ [obj1, obj2, obj3] │ ← 可以从 main 追踪到还活着不回收 ──┘ ╔══════════════════════════════╗ ║ 找不到任何路径能追踪到的对象 ║ ← GC 回收这些 ╚══════════════════════════════╝关键词“可达性”。只要从程序入口能追踪到这个对象GC 就认为它还活着绝对不回收。四、常见的 OOM 真实原因原因 1一次加载数据太多最常见// 我们项目的 OOM 就是这个ListMapString,ObjectsaleListsalesDataGateway.batchSelectMap(query);// pageSize 10000每条 SalesData 有几十个字段// 10000 条 × 5KB/条 50MB 在堆里// 然后 bulk() 把它序列化成 byte[]又占 50MB// 峰值内存 50MB × 3对象 序列化 网络缓冲 150MB// 这批数据还没处理完下一批又进来了// 内存越堆越多 → OOM类比你让搬运工一次搬 10000 箱货他抱不动直接跪倒。原因 2List / Map 无限增长内存泄漏// 全局静态的 Map往里加东西从不清理staticMapString,ObjectcachenewHashMap();voidprocess(Requestreq){cache.put(req.getId(),req.getData());// 一直加// 永远没有 remove}// GC 看到 cache 还活着静态变量 → 永远不回收// cache 里的东西越来越多 → OOM类比仓库HashMap一直进货从不出货终于放不下了。原因 3循环里不断创建大对象for(inti0;i1000000;i){byte[]datanewbyte[1024*1024];// 每次创建 1MB 的数组process(data);// 以为 data 用完就没了// 但 GC 来不及回收// 循环太快内存创建速度 GC 回收速度 → OOM}类比工厂每秒生产 1000 个箱子但清理工每秒只能处理 100 个堆积越来越多仓库炸了。原因 4字符串拼接大报文场景Stringresult;for(Stringline:millionLines){resultresultline;// ❌ 每次都创建新字符串对象}// 前一个 result 虽然没人用了但 GC 还没来得及回收// 新的已经创建出来内存翻倍 → OOM正确做法用StringBuilder原地拼接不产生中间对象。原因 5ByteArrayOutputStream 无限扩容// 我们 ES 写入 OOM 的直接原因// RestHighLevelClient.bulk() 内部ByteArrayOutputStreamoutnewByteArrayOutputStream();for(IndexRequestreq:requests){byte[]jsonserialize(req);// 把每个文档序列化out.write(json);// 往 ByteArrayOutputStream 里写}// ByteArrayOutputStream 内部是 byte[]// 写满了就 Arrays.copyOf 扩容扩成原来的 2 倍// 10000 条数据50MB → 扩容 → 100MB → 扩容 → 200MB → OOM类比快递公司把所有快递都装进一个袋子再发出去袋子越撑越大最后裂开了。五、GC 为什么来不及救场GC 不是随时都在工作的它有触发条件内存分配时序 程序申请内存 │ ▼ Eden 区年轻代满了 │ ▼ 是 触发 Minor GC清理年轻代 │ 还不够Old Gen老年代满了 │ ▼ 是 触发 Full GC清理全部 │ Full GC 后还不够 │ ▼ 是 OOM !!!关键矛盾程序申请内存速度很快循环 批量操作GC 回收速度相对慢需要 STW 停顿有开销当申请速度 回收速度就算 GC 拼命跑也追不上。六、为什么 GC 不回收正在用的对象这是 GC 的安全保证假设 GC 强行回收正在用的对象 Thread A: list.get(0).getName() ↑ GC 突然把这个对象回收了 ↑ Thread A: NullPointerException 崩溃 所以 GC 宁可 OOM也不会回收有引用指向的对象。 这是 Java 内存安全的基础。七、OOM 的本质总结OOM 本质 内存需求 可用内存 两种情况 情况 1真的用了太多内存一次批量太大 解决减少批量大小、流式处理、分批写入 情况 2内存泄漏该释放的没释放 解决检查静态集合、检查缓存是否有上限、 用 WeakReference、及时 close 资源 GC 能做的 ✅ 自动回收没有引用的对象 ❌ 不能回收有引用但逻辑上不用了的对象 ❌ 不能阻止程序一次申请过多内存八、我们项目 OOM 的具体原因和修法原因链 batchSelectMap 查 10000 条 │ 50MB ListMap ▼ bulk() 序列化所有数据到 ByteArrayOutputStream │ ByteArrayOutputStream 扩容 → 50MB → 100MB → 200MB ▼ HTTP 发送还要序列化一遍 │ 再占一份内存 ▼ OOM !!! GC 想回收但上面每一步的对象都有人拿着没法回收。 等到 bulk() 执行完GC 才能回收但那时已经 OOM 了。 修法 1减小 pageSize治标 10000 → 2000峰值内存直接降 5 倍 修法 2BulkProcessor治本 每 500 条 / 每 2MB 自动 flush 一次 flush 完这批对象释放GC 及时回收 下一批再来时内存已经空出来了 峰值内存始终控制在 2MB 级别九、让我们记住这一句话GC 是清道夫但它只清没人要的垃圾。如果你的代码一直抱着数据不放GC 就算再努力也救不了你。真正的解决之道不要一次抱太多。