GC Overhead 排查

线上服务无辜假死状态:一次 GC Overhead 的深度排查

"不是所有的宕机都伴随着 500 错误,有时候,它悄无声息地耗尽了 JVM 的最后一口气。"

最近线上服务出现了一个**"假死"状态的问题。服务没有崩溃,但响应变得极其缓慢,甚至部分任务长时间无响应**。问题没有明显的错误提示,唯一的异常只有一句:

bash 复制代码
bash
 体验AI代码助手
 代码解读
复制代码
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

作为一个已经在 Java 开发路上走了 8 年的程序员,我知道,这句看似"熟悉"的报错,背后往往意味着灾难级的性能问题


🧨 现场回放

时间:2025-09-09 18:42:06

服务接口:/jd-car-monitor/schedule/carRangeGatherAlarm

耗时:126秒

报错堆栈中核心异常如下:

vbnet 复制代码
vbnet
 体验AI代码助手
 代码解读
复制代码
java.sql.SQLException: Error
...
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 异常

相关推荐
standovon13 分钟前
Spring Boot整合Redisson的两种方式
java·spring boot·后端
Cosolar44 分钟前
LlamaIndex RAG 本地部署+API服务,快速搭建一个知识库检索助手
后端·openai·ai编程
MX_93591 小时前
SpringMVC请求参数
java·后端·spring·servlet·apache
忆想不到的晖2 小时前
Codex 探索:别急着调 Prompt,先把工作流收住
后端·agent·ai编程
weixin_408099672 小时前
【实战对比】在线 OCR 识别 vs OCR API 接口:从个人工具到系统集成该怎么选?
图像处理·人工智能·后端·ocr·api·图片文字识别·文字识别ocr
Victor3563 小时前
MongoDB(73)如何设置用户权限?
后端
Victor3564 小时前
MongoDB(74)什么是数据库级别和集合级别的访问控制?
后端
计算机学姐4 小时前
基于SpringBoot的咖啡店管理系统【个性化推荐+数据可视化统计+配送信息】
java·vue.js·spring boot·后端·mysql·信息可视化·tomcat
LSTM974 小时前
使用 Python 将图片转换为 PDF (含合并)
后端
小江的记录本5 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web