🚨
Story 1: 凌晨2点的 OOM(内存溢出)
某天凌晨2点,线上服务突然挂了。手机被告警叫醒...
🧠 JVM 堆内存布局(OOM 根因分析)
JVM Heap — 最大 1GB
Eden 区
256MB
Survivor1
32MB
Survivor2
32MB
Old Gen(老年代)
680MB — 已满!
🔥
245万个对象堆积在 Old Gen,Full GC 回收不了 → OOM!
🐳 Docker 容器总内存限制: 1GB(Heap + Metaspace + 堆外内存 + 系统开销)
2026-03-20 02:05 — 告警触发
1.1 收到告警 — 钉钉机器人告警
服务energy-system-api
状态DOWN
最后心跳2026-03-20 02:03:12
告警时间2026-03-20 02:05:00
服务器192.168.1.100
持续时间已持续 2 分钟
凌晨2点被手机震醒。看到 DOWN 告警,心跳漏了一拍。穿上拖鞋,打开电脑。
02:05
WiFi 🔋 87%
🚨 服务告警 — energy-system-api
状态: DOWN
服务器: 192.168.1.100
请立即处理!
[Prometheus] Alert Firing
Instance 192.168.1.100:8080 is down
凌晨被叫醒排查问题,最忌讳脑子一片空白。深呼吸,然后按流程来:看容器 → 看日志 → 定位代码 → 修复。
1.2 SSH 登服务器
第一时间 SSH 到告警服务器,先看容器状态。
Terminal — ssh ops@192.168.1.100
$ ssh ops@192.168.1.100
Welcome to Ubuntu 22.04 LTS (GNU/Linux 5.15.0-67-generic x86_64)
Last login: Thu Mar 20 01:45:23 2026 from 10.0.0.5
$ docker ps
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 nginx:latest Up 3 hours energy-system_nginx_1
f6e5d4c3b2a1 energy-system:latest Restarting (1) energy-system_app_1 ← 一直在重启!
b2c3d4e5f6a1 mysql:8.0 Up 3 hours energy-system_mysql_1
c3d4e5f6a1b2 redis:7-alpine Up 3 hours energy-system_redis_1
app 容器一直在 Restarting,说明启动就挂。其他服务(Nginx、MySQL、Redis)都正常。问题在 Java 应用本身。看日志。
1.3 看日志 — 找到 OOM 错误
Terminal — docker-compose logs
$ docker-compose logs app --tail=100 | grep -i "error"
2026-03-20 02:03:12 ERROR org.springframework.boot.SpringApplication - Application run failed
2026-03-20 02:03:12 ERROR java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.ArrayList.grow(ArrayList.java:241)
at java.util.ArrayList.grow(ArrayList.java:241)
at com.energy.service.DataExportService.exportAll(DataExportService.java:45)
at com.energy.service.DataExportService$$FastClassBySpringCGLIB$$a1b2c3d.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
2026-03-20 02:03:14 WARN o.s.boot.SpringApplication - Unable to close application context
2026-03-20 02:03:30 INFO o.s.boot.SpringApplication - Starting application...
2026-03-20 02:03:45 ERROR java.lang.OutOfMemoryError: Java heap space
at com.energy.service.DataExportService.exportAll(DataExportService.java:45)
找到了!OOM 发生在 DataExportService.exportAll 第45行。ArrayList 在疯狂 grow,说明往里塞了太多数据。
反复重启都在同一个地方挂掉,说明启动时有定时任务在跑 exportAll()。
1.4 查看内存使用情况
确认一下是不是内存真的不够了。
Terminal — free & docker stats
$ free -h
total used free shared buff/cache available
Mem: 7.8Gi 6.9Gi 120Mi 256Mi 780Mi 760Mi
Swap: 2.0Gi 1.8Gi 200Mi
$ docker stats --no-stream
NAME CPU% MEM USAGE / LIMIT
energy-system_nginx_1 0.02% 24MiB / unlimited
energy-system_app_1 0.0% 1.2GiB / 1.0GiB ← 超了!
energy-system_mysql_1 2.1% 1.8GiB / 4.0GiB
energy-system_redis_1 0.5% 52MiB / 512MiB
内存超了 Docker 限制!JVM 最大堆只设了 1G,但 exportAll() 要把几百万条数据全加载到内存,直接撑爆了。
1.5 查看问题代码
打开 IntelliJ IDEA,定位到 DataExportService.java 第45行。
IntelliJ IDEA — DataExportService.java
DeviceController.java
DataExportService.java
DeviceDataMapper.java
UTF-8 | LF
📂 com.energy
📂 controller
📂 service
📄 DataExportService.java
📂 mapper
📄 DeviceDataMapper.java
📂 config
38public class DataExportService {
39
40 @Autowired
41 private DeviceDataMapper deviceDataMapper;
42
43 // 每天凌晨2点执行数据导出
44 @Scheduled(cron = "0 0 2 * * ?")
45→ public void exportAll() {
46 // ← 一次查了全部数据!245万条!
47 List<DeviceData> allData = deviceDataMapper.selectAll();
48
49 // 全部加载到内存再导出...
50 List<ExportRow> rows = new ArrayList<>();
51 for (DeviceData data : allData) {
52 rows.add(convert(data)); // 数据量太大,OOM了!
53 }
54 excelWriter.write(rows);
55 }
56}
问题很清楚了:
1. selectAll() 一次把 245万条数据全加载到内存
2. 每条数据再转成 ExportRow 对象,内存翻倍
3. 凌晨2点的定时任务触发时,正好数据量从几万涨到了 245万
4. JVM 堆只有 1G,完全不够用,直接 OOM
应该分批查询,边查边写。
1.6 临时修复 + 永久修复
Step A: 临时止血 — 增大内存,先让服务跑起来
$ vim docker-compose.yml
# 修改 JVM 参数,增大堆内存
app:
environment:
- JAVA_OPTS=-Xmx1g -Xms1g
- JAVA_OPTS=-Xmx2g -Xms2g
deploy:
resources:
limits:
memory: 1G
memory: 2.5G
$ docker-compose up -d app
Recreating energy-system_app_1 ... done
✔ Service started successfully
临时修复:先增大内存让服务恢复,但这只是止血。245万条数据全加载到内存不是长久之计。
Step B: 永久修复 — 分批查询 + 流式写入
IntelliJ IDEA — 修复后的 DataExportService.java
DataExportService.java
DeviceDataMapper.java
UTF-8 | LF
43 @Scheduled(cron = "0 0 2 * * ?")
44 public void exportAll() {
45 int pageSize = 1000;
46 int page = 0;
47 excelWriter.createSheet("设备数据");
48
49 while (true) {
50 // 每次只查1000条
51 List<DeviceData> batch = deviceDataMapper.selectPage(
52 page * pageSize, pageSize);
53 if (batch.isEmpty()) break;
54
55 // 转换后立即写入,不在内存中堆积
56 List<ExportRow> rows = batch.stream()
57 .map(this::convert)
58 .collect(Collectors.toList());
59 excelWriter.writeBatch(rows);
60 page++;
61 }
62 }
Git Diff — 代码变更
diff --git a/src/main/java/com/energy/service/DataExportService.java
@@ -42,14 +42,22 @@
@Scheduled(cron = "0 0 2 * * ?")
public void exportAll() {
- List<DeviceData> allData = deviceDataMapper.selectAll();
- List<ExportRow> rows = new ArrayList<>();
- for (DeviceData data : allData) {
- rows.add(convert(data));
- }
- excelWriter.write(rows);
+ int pageSize = 1000;
+ int page = 0;
+ excelWriter.createSheet("设备数据");
+ while (true) {
+ List<DeviceData> batch = deviceDataMapper.selectPage(
+ page * pageSize, pageSize);
+ if (batch.isEmpty()) break;
+ List<ExportRow> rows = batch.stream()
+ .map(this::convert).collect(Collectors.toList());
+ excelWriter.writeBatch(rows);
+ page++;
+ }
}
构建部署
Terminal — Build & Deploy
$ git add . && git commit -m "fix: DataExportService OOM - 分批查询替代全量加载"
[main 3f7a2b1] fix: DataExportService OOM - 分批查询替代全量加载
1 file changed, 12 insertions(+), 6 deletions(-)
$ mvn clean package -DskipTests
[INFO] Scanning for projects...
[INFO] Building energy-system 1.0.0
[INFO] BUILD SUCCESS
[INFO] Total time: 23.4 s
$ docker-compose up -d --build app
Building app
Step 1/8 : FROM openjdk:17-jdk-slim
Step 8/8 : ENTRYPOINT ["java","-jar","app.jar"]
✔ Container energy-system_app_1 Started
$ docker ps | grep app
f6e5d4c3b2a1 energy-system:latest Up 30 seconds energy-system_app_1
↑ 不再 Restarting 了!
服务恢复!验证一下导出功能是否正常。
$ docker-compose logs app --tail=20 | grep "export"
2026-03-20 02:35:00 INFO DataExportService - Export started, pageSize=1000
2026-03-20 02:35:01 INFO DataExportService - Exported page 0, 1000 rows
2026-03-20 02:35:01 INFO DataExportService - Exported page 1, 1000 rows
2026-03-20 02:35:02 INFO DataExportService - Exported page 2, 1000 rows
...
2026-03-20 02:38:15 INFO DataExportService - Exported page 2456, 789 rows
2026-03-20 02:38:15 INFO DataExportService - Export completed! Total: 2456789 rows
$ docker stats --no-stream | grep app
energy-system_app_1 2.3% 512MiB / 2.5GiB ← 内存稳定在 512M