最近线上服务出现了一个**"假死"状态的问题。服务没有崩溃,但响应变得极其缓慢,甚至部分任务长时间无响应**。问题没有明显的错误提示
时间:2025-09-09 18:42:06
服务接口:/schedule/carRangeGatherAlarm
耗时:126秒
报错堆栈中核心异常如下:
bash
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
一开始我以为是数据库问题。但堆栈中 SQL 执行语句并不复杂,关键在于:
- GC Overhead 被触发
- 整个
carRangeGatherAlarm
方法耗时超过 2 分钟 - 日志中无明显 SQL 超时或连接异常
源码分析:业务逻辑是否"无辜"?
我们来看一下这个定时任务的主干逻辑(已简化):
ini
ini
体验AI代码助手
代码解读
复制代码
List<CarStayHistoryForTask> stayHistoryList = carStayHistoryDao.selectAlarmByRangeGather();
for (CarStayHistoryForTask cshTask: stayHistoryList) {
Long stayEndTime = cshTask.getStayEndTime();
if (stayEndTime == null) {
stayEndTime = System.currentTimeMillis();
}
List<CarStayHistory> stayHistoryOtherList = carStayHistoryDao.selectAlarmByRangeGatherOther(...);
// 逻辑判断、地理位置计算、去重、告警处理
...
}
看似没问题,但问题的关键在于:
- 嵌套调用数据库 :每一个
stayHistoryList
里的记录,都要再查一次数据库。 - 极端情况下,
stayHistoryList
的数量可能是成百上千。 - 每次都要从数据库加载大量历史停留记录,再做复杂的地理计算。
这就导致了:内存迅速膨胀,大量对象无法释放,最终触发 GC overhead limit exceeded。
🧠 什么是 GC Overhead Limit Exceeded?
这是 JVM 的一种"自我保护机制",意思是:
"我(JVM)已经花了 98% 的时间在 GC 上,但回收不到 2% 的堆内存,你让我怎么办?"
也就是说,堆内存已经快炸了,JVM 不得不频繁 GC,但就是没法释放空间。这种情况下一般表现为:
- CPU 飙升
- 响应缓慢甚至无响应
- 没有明确报错,但服务"假死"
🔍 深挖背后原因
1. selectAlarmByRangeGather 查询量过大
这个方法一次性查出所有满足条件的驻车数据,如果数据量大,内存直接爆炸。
2. selectAlarmByRangeGatherOther 是 N+1 查询
每个 cshTask
都要再查一次附近的车辆记录,数据库压力大,JVM 压力更大。
3. 地理位置判断代码 耗 CPU
还要判断每辆车是否在某个范围内(圆形区域),涉及数学计算,非常耗时。
4. 没有分页、没有懒加载
数据全部一次性加载到内存,GC 无法跟上,自然就 OOM 了。
如何解决?
✅ 1. 限制处理数据量
给 selectAlarmByRangeGather
增加分页限制,比如每次处理 100 条数据。
sql
sql
体验AI代码助手
代码解读
复制代码
SELECT * FROM car_stay_history WHERE ... LIMIT 100
✅ 2. 使用流式处理(Stream / 游标)
减少一次性加载到内存的数据量,配合 MyBatis 的 ResultHandler
或者 Spring Batch。
✅ 3. 避免 N+1 查询
预加载其他车辆数据,或将逻辑合并为一个大 SQL。
✅ 4. JVM 参数优化
调高堆内存、调整 GC 策略(如 G1GC),避免频繁 Full GC。
ruby
ruby
体验AI代码助手
代码解读
复制代码
-Xms512m -Xmx2048m -XX:+UseG1GC
最终效果
优化后:
- 单次
carRangeGatherAlarm
执行时间从 2 分钟降到 5 秒 - CPU 占用稳定在 30% 以下
- 再无 GC overhead 异常
总结
这次"假死"问题给了我几个深刻的启示:
-
代码看起来没错,不等于没坑
-
线上问题往往不是 crash,而是性能陷阱
-
定时任务和批处理逻辑,最容易被忽略
-
GC overhead 是 JVM 向你发出的最后求救信号