生产环境内存溢出排查实录

目录
  1. 1. 一、问题背景
  2. 2. 二、现象分析
    1. 2.1. 1️⃣ CPU 异常提前出现
    2. 2.2. 2️⃣ 网络流量异常
    3. 2.3. 3️⃣ OOM 时间点
  3. 3. 三、关键排查手段
    1. 3.1. ✅ 1. 请求日志(infra_api_access_log表)
    2. 3.2. ✅ 2. 堆转储文件(heap dump)
  4. 4. 四、问题根因
  5. 5. 五、问题本质
  6. 6. 六、解决方案
    1. 6.1. ✅ 1. 强制限制时间范围(必须)
    2. 6.2. ✅ 2. 接口分页 / 分段查询
    3. 6.3. ✅ 3. 精简返回字段
    4. 6.4. ✅ 4. 增加接口保护
    5. 6.5. ✅ 5. JVM 优化(辅助)
  7. 7. 七、经验总结
    1. 7.1. 🎯 1. “全量查询接口”是 OOM 高发区
    2. 7.2. 🎯 2. 时间维度是隐形杀手
    3. 7.3. 🎯 3. CPU 高 ≠ 计算多
    4. 7.4. 🎯 4. 排查三板斧
  8. 8. 八、常见的 OOM 原因
    1. 8.1. 🎯 1. 大集合 / 全量查询(这次就是典型)
    2. 8.2. 🎯 2. 对象嵌套过深(数据结构设计问题)
    3. 8.3. 🎯 3. JSON 序列化放大
    4. 8.4. 🎯 4. 缓存使用不当(常见坑)
    5. 8.5. 🎯 5. 内存泄漏(对象无法释放)
    6. 8.6. 🎯 6. 大文件 / 大对象处理
    7. 8.7. 🎯 7. 线程过多(间接OOM)
    8. 8.8. 🎯 8. JVM参数设置过小
    9. 8.9. 🎯 9. 频繁创建对象(GC风暴)
    10. 8.10. 🎯 10. 第三方组件问题

一、问题背景

某天线上服务突然出现异常:

  • CPU 持续飙升至 100%
  • 页面无法访问
  • 最终服务崩溃
  • 重启后恢复正常

日志中出现大量异常:

1
java.lang.OutOfMemoryError: Java heap space

JVM 参数如下:

1
-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/heapError -jar /home/pms-server.jar --spring.profiles.active=prod --server.port=48080

二、现象分析

通过监控数据可以观察到:

1️⃣ CPU 异常提前出现

  • 12:57 开始 CPU 持续升高
  • 内存使用率看似稳定

👉 说明:不是简单“内存瞬间打满”,而是GC 风暴,因为查询出来海量数据赋值给变量,即将达到512M,于是GC 开始频繁触发,内存不会被占满,但是CPU 会持续升高。

2️⃣ 网络流量异常

12:56 开始,下行流量暴涨(10MB/s+)

👉 说明: 服务正在返回超大响应数据

3️⃣ OOM 时间点

13:11 出现 OutOfMemoryError

👉 说明: JVM 在 12:57 ~ 13:11 之间经历了持续“内存挣扎”

三、关键排查手段

本次排查主要依赖两个核心工具:

✅ 1. 请求日志(infra_api_access_log表)

定位时间窗口(根据begin_time,而非create_time, 因为create_time 是请求处理完成的时间):

1
12:50 ~ 12:57

发现异常请求:

1
GET /admin-api/hotel/price/dateList

这个请求2026-03-24 12:55:44 开始,2026-03-24 13:13:27 结束,最终报错。

✅ 2. 堆转储文件(heap dump)

因为已经重启了服务,无法直接分析内存溢出的现场,只能通过它的尸体——堆转储文件来分析。
通过 IDEA 打开 heapError.hprof 文件,重点分析:

发现:

1
DatePriceRespVO  Retained Size ≈ 141MB

👉 直接锁定问题对象和请求入口

四、问题根因

🎯 核心问题:dateList 接口返回数据过大

请求参数:

1
2
3
4
5
6
7
8
9
{
"query": {
"checkinTime": "1774332000000",
"checkoutTime": "215099388000000",
"channelType": "4",
"storeId": "10139",
"roomTypeId": "575"
}
}

🚨 致命问题点:

1. 时间范围异常巨大

1
2
checkinTime: 1774332000000
checkoutTime: 215099388000000

👉 时间跨度:极大(远超正常业务范围)

2. JVM 堆限制

1
-Xmx512m

👉 实际可用空间:

  • 老年代 + 新生代 + 线程等
  • 可用业务内存远小于 512M

五、问题本质

👉 本次 OOM 本质是: 接口未限制查询范围,导致超大集合对象生成,引发 GC 风暴,最终 OOM

完整链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
超大时间范围请求

生成大量 DatePriceRespVO

占用 141MB+ 内存

频繁 Full GC

CPU 飙升

GC 无法回收

OutOfMemoryError

六、解决方案

✅ 1. 强制限制时间范围(必须)

1
2
3
if (days > 30) {
throw new IllegalArgumentException("查询时间范围不能超过30天");
}

✅ 2. 接口分页 / 分段查询

❌ 错误方式:一次返回全年价格

✅ 正确方式:按月 / 按周查询

✅ 3. 精简返回字段

1
DatePriceRespVO → 精简DTO

避免:嵌套对象 / 冗余字段

✅ 4. 增加接口保护

  • 限流(Rate Limit)
  • 参数校验
  • 最大返回条数限制

✅ 5. JVM 优化(辅助)

1
2
-Xms2g
-Xmx2g

⚠️ 注意:这只是缓解,不是根治

七、经验总结

🎯 1. “全量查询接口”是 OOM 高发区

典型危险命名:

1
2
3
getAll
listAll
dateList(无限范围)

🎯 2. 时间维度是隐形杀手

1
2
3
4
1天 → 安全
30天 → 可控
365天 → 危险
无限 → 💥

🎯 3. CPU 高 ≠ 计算多

很多时候: CPU 高 = GC 在拼命救场

🎯 4. 排查三板斧

1
2
3
GC日志 → 定时间
访问日志 → 定接口
Heap Dump → 定对象

八、常见的 OOM 原因

🎯 1. 大集合 / 全量查询(这次就是典型)

表现

  • 接口名:getAll / listAll / dateList

  • 返回:List 很大(几十万条)

本质

1
查询范围无边界 → 数据量指数增长 → 内存被吃光

典型代码:

1
List<Room> list = roomMapper.selectList(null);

或:

1
2
3
for (LocalDate d = start; d <= end; d++) {
list.add(...)
}

🔥 特征

  • heap dump 中:ArrayList / HashMap 占用巨大

  • GC日志:

  1. Full GC频繁
  2. old区回收不掉

🎯 2. 对象嵌套过深(数据结构设计问题)

表现

1
2
3
4
5
Room {
List<Price>
List<Order>
List<Something>
}

👉 一次请求返回:ArrayList / HashMap 占用巨大

🔥 特征

  • 单个对象不大,但层级展开后爆炸

  • heap dump:DTO嵌套List

🎯 3. JSON 序列化放大

表现

1
对象 → JSON字符串

👉 内存占用:对象 + JSON = 2倍甚至更多

🔥 特征

  • heap dump: char[] / byte[] 占用巨大

  • 网络流量暴涨(你这次就有)

🎯 4. 缓存使用不当(常见坑)

表现

1
2
Map<String, Object> cache = new HashMap<>();
cache.put(key, value);

👉 没有:

  • 过期策略

  • 大小限制

🔥 特征

  • heap dump: HashMap 占用巨大

  • GC回收不了(强引用)

🎯 5. 内存泄漏(对象无法释放)

表现

对象不再使用,但仍被引用

常见场景

1
2
// 请求结束后,list也不会被回收
static List<Object> list = new ArrayList<>();

或:

1
2
3
4
5
6
7
8
9
10
11
// ThreadLocal 未清理
@Component
public class RequestContext {
// ThreadLocal 会在线程池中传递
private static ThreadLocal<List<Room>> LOCAL_ROOMS = new ThreadLocal<>();

public void setRooms(List<Room> rooms) {
LOCAL_ROOMS.set(rooms); // 设置后,线程复用时不清理
}
}
// Tomcat线程池复用线程,ThreadLocal可能一直存在

🔥 特征

  • Old区持续增长

  • Full GC 后仍不下降

🎯 6. 大文件 / 大对象处理

表现

1
byte[] bytes = file.readAllBytes();

或:

1
一次性加载大Excel / 大图片

🔥 特征

  • heap dump:byte[] 巨大

🎯 7. 线程过多(间接OOM)

表现

1
new Thread(...).start();

或线程池无限制

🔥 特征

  • 线程数暴涨

  • OOM类型:unable to create new native thread

🎯 8. JVM参数设置过小

表现

1
-Xmx512m

👉 正常业务也可能撑不住

🔥 特征

  • 数据量不大也OOM

  • 提高内存后明显改善

🎯 9. 频繁创建对象(GC风暴)

表现

1
2
3
for (...) {
new Object();
}

👉 对象短命但数量极多

🔥 特征

  • CPU 100%

  • 内存看起来不高(你这次就是)

🎯 10. 第三方组件问题

常见

  • Redis 客户端(如 Redisson)

  • ORM(MyBatis / Hibernate)

  • JSON库