记一次线上OOM排查,JVM调优全过程

上周三下午,正在摸鱼,突然钉钉群里炸了:

css 复制代码
[告警] 订单服务 POD重启
[告警] 订单服务 POD重启
[告警] 订单服务 POD重启

3个Pod连续重启,打开监控一看,内存直接打满然后被K8s杀掉了。

经典的OOM。

现象

  • 服务:订单服务(Java,Spring Boot)
  • 部署:K8s,3个Pod,每个限制4G内存
  • 现象:内存缓慢增长,到达4G后被OOM Kill
  • 频率:每隔2-3小时重启一次

第一反应:加内存?

领导说:内存不够就加嘛。

我:...加内存治标不治本,得找到根因。

排查过程

1. 先看GC日志

bash 复制代码
# JVM参数里加上GC日志
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-Xloggc:/logs/gc.log

等了一个小时,服务挂了,捞出GC日志:

csharp 复制代码
[Full GC (Allocation Failure) 3800M->3750M(4096M), 5.234 secs]
[Full GC (Allocation Failure) 3780M->3760M(4096M), 5.567 secs]
[Full GC (Allocation Failure) 3790M->3785M(4096M), 6.012 secs]

Full GC后内存几乎没释放,说明有内存泄漏,有东西一直占着不放。

2. dump内存

在容器里加了个脚本,OOM前自动dump:

bash 复制代码
# JVM参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heap.hprof

但K8s的OOM Kill太快了,还没来得及dump就被杀了。

换个方式,手动dump:

bash 复制代码
# 找到Java进程PID
jps

# 手动dump(等内存涨到3G左右时执行)
jmap -dump:format=b,file=/logs/heap.hprof <pid>

dump出来一个3G的文件。

3. 分析heap dump

把文件拷到本地,用MAT(Memory Analyzer Tool)打开。

yaml 复制代码
Leak Suspects Report:

Problem Suspect 1:
  256,789 instances of "com.xxx.OrderDTO"
  占用内存:2.1 GB (54%)

25万个OrderDTO对象?这订单量也没这么大啊。

点进去看引用链:

rust 复制代码
java.util.concurrent.ConcurrentHashMap
  -> com.xxx.cache.LocalCache
    -> OrderDTO (256789 instances)

LocalCache,本地缓存。

4. 定位代码

java 复制代码
// LocalCache.java
public class LocalCache {
    private static final Map<String, OrderDTO> cache = new ConcurrentHashMap<>();
    
    public static void put(String orderId, OrderDTO order) {
        cache.put(orderId, order);
    }
    
    public static OrderDTO get(String orderId) {
        return cache.get(orderId);
    }
    
    // 没有remove方法
    // 没有过期机制
    // 没有大小限制
}

经典错误:只往缓存里放,不清理。

一查代码提交记录,是3个月前一个同事为了"优化性能"加的,从那之后这个服务就开始间歇性OOM。

5. 修复

方案一:直接删掉这个缓存(最简单)

方案二:换成带过期的缓存

java 复制代码
// 用Caffeine替代
private static final Cache<String, OrderDTO> cache = Caffeine.newBuilder()
    .maximumSize(10000)           // 最多1万条
    .expireAfterWrite(5, TimeUnit.MINUTES)  // 5分钟过期
    .build();

public static void put(String orderId, OrderDTO order) {
    cache.put(orderId, order);
}

public static OrderDTO get(String orderId) {
    return cache.getIfPresent(orderId);
}

上线后,内存稳定在1.5G左右,再也没OOM过。

JVM参数调优

趁这个机会,把JVM参数也优化了一下。

之前的配置

bash 复制代码
-Xms2g -Xmx4g
# 就这两个参数...

优化后的配置

bash 复制代码
# 堆内存
-Xms4g -Xmx4g              # 初始和最大一样,避免动态调整
-XX:NewRatio=2             # 年轻代:老年代 = 1:2

# GC选择(JDK11+推荐G1)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200   # 目标停顿时间200ms
-XX:G1HeapRegionSize=8m    # Region大小

# 元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m

# GC日志
-Xlog:gc*:file=/logs/gc.log:time,level,tags:filecount=5,filesize=100m

# OOM时dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/

# 容器感知(JDK8u191+)
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0  # 使用容器内存的75%

监控指标

加了几个关键监控:

yaml 复制代码
# Prometheus指标
- jvm_memory_used_bytes
- jvm_gc_pause_seconds_sum
- jvm_gc_pause_seconds_count
- jvm_classes_loaded_classes_total

设置告警:

  • 老年代使用率 > 80% 持续5分钟
  • Full GC 频率 > 1次/分钟
  • GC停顿时间 > 1秒

排查工具汇总

场景 工具 命令
查看堆内存 jmap jmap -heap
dump内存 jmap jmap -dump:format=b,file=heap.hprof
分析dump MAT 图形界面
实时监控 jstat jstat -gcutil 1000
查看线程 jstack jstack
在线分析 Arthas dashboard / heapdump

Arthas神器

推荐用Arthas,在线诊断特别方便:

bash 复制代码
# 下载启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 选择要attach的进程后:

# 查看堆内存
dashboard

# 查看最占内存的对象
heapdump --live /tmp/heap.hprof

# 查看某个类的实例数
sc -d com.xxx.OrderDTO
vmtool --action getInstances --className com.xxx.OrderDTO --limit 10

# 查看方法调用
watch com.xxx.LocalCache put '{params, returnObj}' -x 2

远程调试

这次OOM排查还好在公司,能直接连到服务器。

如果在家收到告警,K8s集群在公司内网,怎么办?

之前的方案是远程专线,但经常断,而且手机上操作kubectl很难受。

后来用星空组网把电脑和跳板机组到一起,在家也能kubectl进容器排查了。

总结

这次OOM排查的经验:

步骤 工具/方法
1. 确认OOM 监控告警、GC日志
2. dump内存 jmap / HeapDumpOnOutOfMemoryError
3. 分析dump MAT / jhat
4. 定位代码 引用链分析
5. 修复上线 代码修改
6. 加固监控 JVM监控指标

常见的内存泄漏原因:

  • 缓存无限增长(本次)
  • 静态集合持续添加
  • 未关闭的连接/流
  • ThreadLocal使用不当
  • 监听器未注销

最后,写缓存的时候一定要想清楚:

  • 什么时候过期?
  • 最大存多少?
  • 怎么清理?

不然就等着半夜被电话叫醒吧。


有JVM相关问题欢迎评论区交流。

相关推荐
韩立学长2 小时前
【开题答辩实录分享】以《植物园信息管理系统》为例进行选题答辩实录分享
java·数据库·spring
a程序小傲2 小时前
京东Java面试被问:垃圾收集算法(标记-清除、复制、标记-整理)的比较
java·算法·面试
austin流川枫2 小时前
深度解析六大Java微服务框架
java·后端·微服务
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 高校贫困生资助管理系统为例,包含答辩的问题和答案
java·eclipse
weixin_448119942 小时前
Datawhale Hello-Agents入门篇202512第2次作业
java·前端·javascript
hopsky2 小时前
数据中台权限设计
java·权限设计
Brookty2 小时前
Java文件操作系列(一):从基础概念到File类核心方法
java·学习·java-ee·文件io
小鸡脚来咯2 小时前
java泛型详解
java·开发语言
爱笑的眼睛112 小时前
JAX 函数变换:超越传统自动微分的编程范式革命
java·人工智能·python·ai