
如果你有三年以上 Java 后端经验,那你大概率见过线上 OOM。最离谱的是,我们组某次凌晨三点报警,服务 OOM 了三次,业务同学说:“平时都正常,今晚也没啥流量,为啥突然 OOM?”我当时迷迷糊糊地盯着日志,看到 java.lang.OutOfMemoryError: Java heap space 那一瞬间,困意立刻没了——因为我知道,这种“偶尔性 OOM”八成不会是简单问题。
结论先说:Java 的内存问题,从来不是“平时没事,今天突然坏了”,而是某个业务或对象长期积累到一定程度才爆。就像水杯一直往里滴水,快满的时候轻轻放个针就能溢。
这篇文章就是把这些年踩的 Java 内存坑,全部摊开讲。
线上 OOM 不会给你“我要 OOM 了”的通知,它的表现非常随机,非常恶心。
常见几个表现:
promotion failed 或 to-space exhausted我记得一次服务 QPS 正常,CPU 也正常,但 Full GC 时间从 200ms 飙到 4s,平均 RT 被拖到 600ms,我当时就知道肯定要出事。三分钟后,OOM 真来了。
OOM 不是突然发生的,它是内存给你“我顶不住了”的最后尖叫。
别问为什么要看这些区域,线上定位问题时不知道堆、栈、元空间的区别,基本就等于在黑屋里找黑猫。
我自己的经验是,Java 内存问题 80% 都不是栈的问题,剩下 20% 在堆和元空间,还有少量在 Direct Memory。
几个对排查特别关键的点:
不用背,排查的时候每一处都能对应一种 OOM。
我遇到过至少七种常见 OOM,区别很大。
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Unable to create new native thread
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: PermGen space(老版本)几个关键点:
不同的 OOM 对应不同方向,搞错方向就会查到怀疑人生。
说流程前先吐槽一句:线上 dump 文件真的巨大,一次服务 OOM,dump 出来一个 7GB 的 hprof 文件,我 scp 下来花了 9 分钟,MAT 打开又花了 8 分钟……你能体会这种绝望。
排查流程大概是:
jmap -dump:format=b,file=oom.hprof <pid>如果容器 kill,你只能在 k8s events 或 docker logs 里看是否 OOMKilled。
找“谁占了最多空间”。真正泄漏的对象大部分会挂在一个引用链下,比如某个 Map。
jstat -gcutil <pid> 1000看 Old 区是否一直增长,看 Full GC 是否频繁。
MAT 有一个很逆天的功能,能看到对象创建的位置栈。
只有定位到“对象在哪里被创建的”才能真正修复,而不是盲目扩容。
很多人以为对象创建没啥成本,但如果大量短期对象挤爆了年轻代,会触发频繁 YGC,当晋升阈值被打满,就会导致大量对象升入 Old 区,Old 区慢慢被撑满,最终 OOM。
有次我们某个接口 QPS 从 500 涨到 6000,RT 从 30ms 涨到 200ms,日志里一堆 StringBuilder、JSON parse 的对象。我在 CPU 火焰图看到 40% 的 CPU 都在 JSON 反序列化。堆不是很大,但因为对象太多,一分钟 200 次 YGC,老年代直接被挤爆。
减少对象创建,尤其是大对象,是最快速降低堆压力的方式。
大对象(BigObject)有时候不会走年轻代,而是直接进入老年代,这就意味着你一次创建一个 20MB 的对象,老年代一下被吃掉 20MB。
我见过一次 OOM 是因为某人写了类似这样的错误代码:
byte[] body = new byte[50 * 1024 * 1024]; // 50MB只是为了临时拼装一个输出,然后没释放,结果整个系统都跪了。
这点网上讲得少,但线上很常见。
OOM 类型通常是:
java.lang.OutOfMemoryError: Unable to create new native thread问题根源:
我遇到一个超离谱的:某服务用线程池 cachedThreadPool,在峰值时瞬间启动了 1500 多条线程,系统直接炸成 OOM。根本不是堆问题,是线程栈空间耗尽。
做网关或 RPC 的人应该深有体会。
Netty 的 ByteBuf 分成 heap buffer 和 direct buffer,direct buffer 在堆外,用的是 Direct Memory。这个东西不会计算进 Java heap,所以你看到堆稳得很,但系统突然 OOM。
经典错误写法:
ByteBuf buf = Unpooled.directBuffer(1024 * 1024);如果你不 release,它就永远不回收。
我见过一个网关服务,每分钟分配几百个 direct buffer,但没有 release,运行两个小时直接被系统 OOM Killed,堆一点问题没有。
泄漏(Leak)= 对象不该活这么久
堆满(Heap full)= 正常创建对象,只是量大
你用 MAT 很容易区分:
一个小技巧是看 GC 日志:
我自己偏向稳妥配置,而不是一味堆大堆:
-Xms4g
-Xmx4g
-XX:MaxDirectMemorySize=2g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-Xss512k几个经验点:
我常看见几个造成泄漏的源头:
一个我自己遇到的笑不出来的案例:某次我们用 Guava Cache,本来 TTL 设置 10 分钟。结果有个同事 copy 代码时把 expireAfterWrite 删了,缓存里的对象越积越多,一个下午涨了 7GB,直接堆爆炸。
有一次线上 OOM,把我折磨了整整六小时。
表现:
dump 打开 MAT 后看到一个 HashMap 占了整整 1.8GB 的对象。引用链最终指向一段代码:
Map<String, Object> cache = new HashMap<>();
cache.put(key, JSON.parseObject(body));问题是 body 是可变的,而且每次都是一个 200KB 的 JSON,服务又没做 LRU 和 TTL。结果这个 HashMap 存了两万个对象,总体积大概 4GB。
根本不是“偶尔性 OOM”,是对象吃满后爆发。
光靠 JVM heap 监控不够用,我线上一般会监控这个:
监控到位,能提前让你知道系统 10 分钟后可能要 OOM。
Java 内存问题真不是拍脑袋解决的,而是靠 dump、MAT、GC 日志、监控把它找出来。大部分线上 OOM 都不是突然事件,而是长时间积累出来的隐患。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。