☰
JVM 内存持续增长与 Full GC 频繁
2026/10/27 1:58:12 网站建设 项目流程

JVM 内存持续增长与 Full GC 频繁

一、故障场景

某 Spring Boot 服务运行几个小时后,接口响应时间明显变慢。

线上监控现象:

  • JVM 堆内存使用量持续增长。
  • Full GC 越来越频繁。
  • Full GC 后,堆内存仍然很高。
  • 接口响应时间从 100ms 上升到 3~5 秒。
  • CPU 使用率从 30% 上升到 85%。
  • 重启服务后恢复正常,但几个小时后再次出现。
  • 业务请求量没有明显增长,最近也没有发布新版本。

二、初步分析

1. Full GC 后内存仍然很高,说明什么?

说明大量对象在 GC 后仍然存活,或者存在较大的存活对象集合,需要进一步分析。

常见原因:

  • 内存泄漏:对象已经没有业务价值,但仍被引用,无法回收。
  • 缓存无限增长:缓存没有容量限制或清理机制。
  • 对象生命周期管理不合理:任务结束后,对象仍然被长期持有。
  • 正常业务数据量较大:系统确实需要保留大量对象。

注意:Full GC 后内存高,不等于一定存在内存泄漏。

2. 为什么不能直接扩容?

假设 JVM 堆内存从 4GB 扩大到 8GB,如果根因是对象持续积累,扩容只能延缓故障发生。

正确思路:

  1. 确认存活对象是否持续增长。
  2. 找出占用内存最多的对象。
  3. 分析对象为什么无法被回收。
  4. 根据业务生命周期修复代码或缓存设计。
  5. 评估是否需要调整堆内存大小。

三、生产环境排查流程

第一步:分析 GC 日志

重点关注:

  • GC 发生频率是否持续增加。
  • 每次 Full GC 回收了多少内存。
  • Full GC 后的存活堆内存是否持续增长。
  • GC 暂停时间是否越来越长。

例如:

时间Full GC 后堆内存
10:001.2GB
11:002.0GB
12:002.8GB
13:003.5GB

如果业务请求量基本不变,但 GC 后的存活堆内存持续增长,就需要重点调查对象积累问题。

注意:单次数据不足以证明内存泄漏,应结合长期趋势和对象分析。

第二步:生成并分析 Heap Dump

Heap Dump 是 JVM 某一时刻的堆内存快照,可用于分析对象分布、引用关系和内存占用。

重点查看:

  • 对象实例数量。
  • 对象的 Shallow Heap。
  • 对象的 Retained Heap。
  • Dominator Tree。
  • GC Roots 引用链。

常用工具:Eclipse MAT。

生产环境生成 Heap Dump 可能引起明显停顿,并占用大量磁盘空间,需要提前评估风险。

第三步:定位异常对象

假设 MAT 显示:

  • OrderContext实例数量特别多。
  • 占用堆内存的 60%。
  • Full GC 后仍有大量实例存活。

此时不能直接认定OrderContext存在内存泄漏。

需要继续检查:

  1. 这些对象是否符合正常业务规模?
  2. 对象数量是否随运行时间持续增长?
  3. 哪些对象或容器一直持有它们?
  4. 对象对应的业务任务是否已经结束?
  5. 对象是否有合理的清理机制?

第四步:分析 GC Roots 引用链

例如:

GC Root ↓ OrderService.contextCache(静态 Map) ↓ Map.Entry ↓ OrderContext ↓ 订单相关对象

这说明OrderContext被静态 Map 持有,因此 GC 无法回收这些对象。

静态 Map 本身不一定有问题。关键是检查:

  • 缓存是否有容量限制?
  • 订单处理结束后,是否移除了对应对象?
  • 异常路径是否遗漏清理?
  • 是否存在并发情况下重复写入或清理失败?
  • 缓存中的对象是否已经失去业务价值?

判断内存泄漏的关键证据:对象持续增长、已经不再需要却长期存活,并且引用链能解释对象为什么无法回收。


四、常见排查工具

工具用途
GC 日志分析 GC 频率、暂停时间和回收效果
jcmd获取 JVM 信息、生成 Heap Dump 等
Eclipse MAT分析对象数量、Retained Heap、Dominator Tree 和引用链
Arthas查看 Java 运行状态、线程及类相关信息

五、实战案例:ConcurrentHashMap 导致对象持续增长

1. 问题代码设计

假设服务使用一个静态 Map 保存订单上下文:

privatestaticfinalConcurrentHashMap<Long,OrderContext>contextMap=newConcurrentHashMap<>();

订单开始处理时,将上下文放入 Map:

contextMap.put(orderId,context);

但订单处理完成后,没有移除对应对象。

随着订单不断增加:

订单 1 → OrderContext 订单 2 → OrderContext 订单 3 → OrderContext ...

静态 Map 一直持有这些对象,导致它们无法被 GC 回收。

2. 修复思路

如果订单处理完成后不再需要上下文,应及时移除:

try{processOrder(orderId);}finally{contextMap.remove(orderId);}

这只是简化示例。真实业务中,必须根据任务状态、失败重试和并发处理方式设计清理时机。

3. 异常路径处理

不能只在成功路径清理。

例如:

try{processOrder(orderId);}catch(Exceptione){// 记录异常,并根据业务设计处理重试或失败状态throwe;}finally{// 确认上下文不再被需要后,才执行清理}

如果失败后仍需要重试,应先明确上下文是否需要保留或持久化,不能直接删除必要的数据。

4. 并发安全

如果多个线程可能同时处理同一个订单,需要防止:

  • 一个线程清理上下文,另一个线程仍在使用。
  • 已完成的任务被其他线程重新写入 Map。
  • 失败重试与清理操作相互竞争。

需要明确任务状态、并发控制和上下文所有权。


六、为什么不能简单地换成 Redis?

把 JVM 内存中的 Map 换成 Redis,只是改变了数据存储位置。

如果数据持续写入且没有过期、淘汰或清理机制,Redis 同样可能出现内存持续增长。

应根据数据性质选择存储方式:

数据类型处理思路
正在处理的订单上下文在有效业务生命周期内保留
已处理完成的临时上下文不再需要时及时清理
短期缓存数据根据业务要求设置容量限制和过期策略
必须长期保存的业务数据持久化到数据库等合适的存储中

注意:不能不加区分地为所有对象设置 TTL,否则可能导致仍在处理的业务数据被提前清理。


七、完整故障处理思路

发现 Full GC 频繁、GC 后内存仍然很高 ↓ 分析 GC 日志,观察存活堆内存趋势 ↓ 生成并分析 Heap Dump ↓ 找出占用内存最多的对象类型 ↓ 查看 GC Roots 和对象引用链 ↓ 判断是正常存活对象、缓存增长还是生命周期管理问题 ↓ 修复引用持有、缓存淘汰或对象清理逻辑 ↓ 验证 GC 后内存、对象数量和接口延迟是否恢复

八、核心知识总结

  1. Full GC 后内存仍然很高,需要调查存活对象,但不能直接断定内存泄漏。
  2. CPU 高可能是频繁 GC 导致的结果,而不一定是最初的根因。
  3. Heap Dump 可以帮助定位大量对象及其引用关系。
  4. GC Roots 引用链可以解释对象为什么无法被回收。
  5. 静态集合、缓存、监听器和任务队列都可能长期持有对象。
  6. 业务处理结束后,应释放不再需要的对象引用。
  7. 异常路径、重试机制和并发操作都必须纳入生命周期管理。
  8. 扩容只能增加容量,不能替代根因修复。
  9. 更换 Redis 也不能自动解决无限增长问题。

一句话记忆:先看 GC 趋势,再找大对象,沿引用链找持有者,最后根据业务生命周期决定如何清理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询