记一次线上Java OOM问题排查全过程|附排查工具和方法论
那天正准备下班,运维群里突然炸了:生产环境服务挂了,用户反馈页面打不开。
看了眼监控,Full GC疯狂触发,内存占用100%,典型的OOM。
折腾了3个小时才定位到问题,记录一下排查过程,希望对大家有帮助。
一、现象
告警信息:
java.lang.OutOfMemoryError: Java heap space
监控表现:
- 堆内存使用率:100%
- Full GC次数:每分钟50+次
- 服务响应时间:从200ms飙到10s+
- 最终服务不可用
二、紧急处理
先恢复服务再说。
# 1. 保留现场(dump内存快照)
jmap -dump:format=b,file=/tmp/heapdump.hprof $(pgrep -f my-service)
# 2. 重启服务
systemctl restart my-service
# 3. 确认恢复
curl http://localhost:8080/actuator/health
踩坑提醒:
- dump之前一定要先保留现场!重启后就没了
- 生产环境dump会导致STW,服务会卡住几秒到几分钟
- 如果服务已经完全卡死,可能dump不出来
三、排查过程
3.1 先看GC日志
# JVM启动参数要加GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log
GC日志分析:
2024-01-15T16:02:31.123: [Full GC (Allocation Failure)
[PSYoungGen: 87360K->87360K(87360K)]
[ParOldGen: 174752K->174720K(174784K)]
262112K->262080K(262144K), 0.8234560 secs]
发现问题:
- Full GC后内存几乎没释放
- 老年代几乎满了
- 说明有大对象一直无法回收
3.2 分析Heap Dump
用MAT(Memory Analyzer Tool)打开dump文件:
下载地址:https://www.eclipse.org/mat/
步骤:
- File → Open Heap Dump
- 选择 "Leak Suspects Report"
- 查看可疑对象
MAT分析结果:
Problem Suspect 1:
One instance of "java.util.ArrayList" loaded by "<system class loader>"
occupies 1,234,567,890 bytes (89.12% of the heap).
The memory is accumulated in one instance of "java.util.ArrayList"
loaded by "<system class loader>".
一个ArrayList占了89%的内存?
3.3 定位代码
点击查看这个ArrayList的引用链:
java.util.ArrayList
↑ com.example.service.CacheService.dataCache
↑ com.example.service.CacheService (singleton)
找到了!是CacheService里的一个缓存List。
3.4 查看代码
@Service
public class CacheService {
// 问题就在这里!
private List<DataDTO> dataCache = new ArrayList<>();
@Scheduled(fixedRate = 60000) // 每分钟执行
public void loadData() {
List<DataDTO> newData = dataRepository.findAll();
dataCache.addAll(newData); // 只加不清!
}
public List<DataDTO> getCache() {
return dataCache;
}
}
问题根因:
- 定时任务每分钟往List里加数据
- 但是没有清理逻辑
- 时间一长,List越来越大
- 最终把堆内存撑爆
四、修复方案
方案1:加载前清空
@Scheduled(fixedRate = 60000)
public void loadData() {
List<DataDTO> newData = dataRepository.findAll();
dataCache.clear(); // 先清空
dataCache.addAll(newData);
}
方案2:直接替换引用(推荐)
@Scheduled(fixedRate = 60000)
public void loadData() {
List<DataDTO> newData = dataRepository.findAll();
dataCache = new ArrayList<>(newData); // 直接替换
}
方案3:用Guava Cache(更规范)
private Cache<String, List<DataDTO>> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
@Scheduled(fixedRate = 60000)
public void loadData() {
List<DataDTO> newData = dataRepository.findAll();
cache.put("data", newData);
}
五、复盘:为什么没早发现?
5.1 监控不到位
之前只监控了CPU和磁盘,没监控JVM内存。
改进:加上JVM监控
# Prometheus + JMX Exporter
scrape_configs:
- job_name: 'jvm'
static_configs:
- targets: ['localhost:9090']
关键指标:
jvm_memory_used_bytes- 内存使用量jvm_gc_collection_seconds_count- GC次数jvm_gc_collection_seconds_sum- GC耗时
5.2 缺少告警
没配置内存告警,等OOM了才知道。
改进:Alertmanager配置
- alert: JvmMemoryHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "JVM堆内存使用超过80%"
5.3 Code Review不够细
这段代码其实很明显有问题,但Code Review时没发现。
改进:加入静态分析工具
<!-- SpotBugs -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
六、OOM排查工具箱
6.1 命令行工具
# 查看Java进程
jps -l
# 查看堆内存使用
jmap -heap <pid>
# 查看对象统计
jmap -histo <pid> | head -20
# dump内存快照
jmap -dump:format=b,file=heap.hprof <pid>
# 实时查看GC
jstat -gcutil <pid> 1000
# 查看线程栈
jstack <pid> > thread.txt
6.2 可视化工具
| 工具 | 用途 | 推荐度 |
|---|---|---|
| MAT | 分析Heap Dump | ⭐⭐⭐⭐⭐ |
| VisualVM | 实时监控JVM | ⭐⭐⭐⭐ |
| JProfiler | 性能分析(收费) | ⭐⭐⭐⭐⭐ |
| Arthas | 在线诊断 | ⭐⭐⭐⭐⭐ |
6.3 Arthas神器
阿里开源的Java诊断工具,强烈推荐:
# 安装
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 查看堆内存
dashboard
# 查看最占内存的对象
heapdump /tmp/dump.hprof
# 查看方法调用
trace com.example.CacheService loadData
七、常见OOM类型
| 类型 | 原因 | 解决方案 |
|---|---|---|
| Java heap space | 堆内存不足 | 调大-Xmx或优化代码 |
| Metaspace | 类元数据区满 | 调大-XX:MaxMetaspaceSize |
| GC overhead limit | GC耗时过长 | 优化对象创建 |
| Direct buffer memory | 直接内存溢出 | 调大-XX:MaxDirectMemorySize |
| Unable to create native thread | 线程太多 | 减少线程或调大ulimit |
八、预防措施
8.1 代码规范
- 集合类注意及时清理
- 用完的资源及时close
- 避免在循环里创建大对象
- 字符串拼接用StringBuilder
8.2 JVM参数
# 生产环境推荐配置
-Xms4g -Xmx4g # 堆内存,建议Xms=Xmx
-XX:+HeapDumpOnOutOfMemoryError # OOM时自动dump
-XX:HeapDumpPath=/tmp/ # dump文件路径
-XX:+PrintGCDetails # GC日志
-Xloggc:/var/log/gc.log # GC日志路径
8.3 监控告警
- 堆内存使用率 > 80% 告警
- Full GC次数 > 10次/分钟 告警
- GC耗时 > 1秒 告警
九、远程排查技巧
有时候问题服务器在远程,本地连不上怎么办?
方案:用组网工具把本地和服务器连起来
我们测试环境在公司内网,但我在家办公。用星空组网把我的Mac和测试服务器组到一个虚拟局域网:
# 测试服务器(内网)
192.168.188.20
# 我的Mac
192.168.188.100
然后就可以:
- 直接ssh连上去跑命令
- 用VisualVM远程连接JMX
- 把dump文件scp到本地分析
比开VPN方便多了,速度也快。
总结
OOM排查流程:
- 保留现场 - 先dump再重启
- 看GC日志 - 确认是内存问题
- 分析Dump - 用MAT找大对象
- 定位代码 - 顺着引用链找
- 修复上线 - 验证问题解决
- 加监控告警 - 防止再次发生
这次事故虽然折腾了3小时,但也学到了不少。希望这篇文章能帮到遇到同样问题的朋友。
有问题评论区交流~

浙公网安备 33010602011771号