线上服务一 OOM,最怕的不是机器挂掉,而是整个团队站在工位前,盯着监控面板不知道下一秒该点哪里。干过几年后端的人基本都有这种体会:翻日志、查监控、猜代码,三件事来回折腾,运气好半小时找出凶手,运气不好一整天过去,服务重启了三四轮,问题还在那儿。这篇文章就是把线上 OOM 定位这件事彻底聊透。我会从 OOM 的类型判断、事前埋点、dump 分析、实战复盘到延伸场景全部过一遍,全程基于真实可行的经验,不绕弯子,保证你下次再遇到线上 OOM,不是靠感觉,而是靠流程。
先把我自己的习惯放前面:OOM 定位这个事,七分在事前,三分在事中。真正跑线上的人都知道,现场转瞬即逝,没有提前准备好的埋点和监控,等事故发生了再上去抓现场,基本等于大海捞针。
1. 线上 OOM 的常见形态:先想清楚到底炸的是哪块内存
很多人一看到 OutOfMemoryError 就默认是堆内存不够,实际上 OOM 是一个家族,Java 虚拟机在运行时不同区域都有各自的溢出形态。定位的第一步不是拿工具分析 dump,而是先回答一个问题:这次 OOM 炸的是哪个区?
1.1 三代 OOM:堆溢出、元空间溢出、直接内存溢出
第一类,java.lang.OutOfMemoryError: Java heap space,这是最典型、最常见的一种。堆内存分为新生代和老年代,新生代存短命对象,老年代存熬过多次 GC 的长命对象。堆溢出的本质是:对象大量产生且无法被回收,GC 反复尝试后仍然腾不出空间。出现这种错误时,服务通常会频繁 Full GC,然后在某一次分配对象时直接抛异常退出。
第二类,java.lang.OutOfMemoryError: Metaspace,这是元空间的内存不够了。元空间存的是类的元数据、方法信息、常量池这些 JVM 层面的东西。线上遇到这种 OOM,多半是动态生成类导致的,比如 CGLIB 代理类、反射生成的大量代理对象、热部署场景下的类加载器泄漏。
第三类,java.lang.OutOfMemoryError: Direct buffer memory,直接内存溢出。这个最坑爹,因为它不在堆内,默认情况下也不受 -Xmx 限制,NIO 的 ByteBuffer、Netty 的堆外内存都会从这块走。很多团队只盯着堆大小调参,忽略了 -XX:MaxDirectMemorySize,结果堆只有 2G,堆外却塞了 10G,最后被操作系统杀掉。
另外还有两种容易混淆的情况:一种是 java.lang.OutOfMemoryError: GC overhead limit exceeded,这实际上是 GC 濒死挣扎的表现——JVM 花超过 98% 的时间在 GC 上,但回收回来的内存不足 2%;另一种是创建线程时出现的 Unable to create new native thread,本质是操作系统的线程数和进程内存上限被耗尽了,严格说也属于 native memory 的问题。
注意:判断 OOM 类型不是靠猜的。第一步永远是看日志,找到你服务里
OutOfMemoryError后面的完整错误文案。不同文案对应完全不同的排查路径,搞错了方向就会白忙活半天。
1.2 为什么线上 OOM 比本地复现要难十倍
本地环境复现 OOM 相对容易,因为你可以控制并发量、控制数据规模、反复试错。但线上环境完全不一样,几个因素叠加在一起,把定位难度拉满了:
第一,流量是不可预测的。平时每天几十万请求的服务,某天突然来了一波活动流量,热点商品、秒杀接口、大促链路,每一层的对象分配速率都可能飙升。这种突发流量导致的 OOM,你在本地用 JMeter 压测都很难精准模拟出同样的内存分配曲线。
第二,数据特征不同。本地测试用的都是脱敏的、抽样的小数据,但线上真实数据往往存在大量边界情况。举个例子,一个接口本来正常返回 100 条记录,某天上游产线数据异常,一条记录里嵌套了几万条子记录,JSON 反序列化直接撑爆了内存。这种问题是数据内容触发的,代码层面的逻辑在本地怎么测都是好的。
第三,线程数和请求堆积的问题本地基本测不出来。Tomcat 默认的线程池是 200,当上游接口变慢、数据库连接耗尽时,请求全部堆积在队列里,每个请求占着部分内存不释放,量一起来直接 OOM。这种“慢服务拖垮内存”的连锁反应,本地环境根本模拟不出来。
所以,线上 OOM 定位必须有一套标准化的打法,不能靠灵感和运气。接下来的内容就是把这套打法一步步拆开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 事前准备做足三分:三件套必不可少
线上 OOM 定位最大的痛点是现场消失。进程一挂,内存里的一切都没了,再牛的分析师也只能对着重启后的空状态发呆。所以真正高效的团队,都会提前在 JVM 启动参数和监控体系上做足功夫,保证 OOM 发生的那一刻,证据能留下来。
2.1 启动参数:JVM 崩溃前必须留下 dump 文件
所有 Java 服务,无论用什么框架跑,启动参数里都应该加上 this 组配置。我自己的标配是这样的:
bash复制-Xms
