记一次线上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/

步骤:

  1. File → Open Heap Dump
  2. 选择 "Leak Suspects Report"
  3. 查看可疑对象

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排查流程:

  1. 保留现场 - 先dump再重启
  2. 看GC日志 - 确认是内存问题
  3. 分析Dump - 用MAT找大对象
  4. 定位代码 - 顺着引用链找
  5. 修复上线 - 验证问题解决
  6. 加监控告警 - 防止再次发生

这次事故虽然折腾了3小时,但也学到了不少。希望这篇文章能帮到遇到同样问题的朋友。

有问题评论区交流~

posted @ 2025-12-11 15:01  花宝宝  阅读(3)  评论(0)    收藏  举报