大家好,今天咱们聊一个让无数Java程序员闻风丧胆的问题——**JVM内存OOM(OutOfMemoryError)**。
想象一下这个场景:周五晚上8点,你正准备关机下班,突然钉钉群炸了:"线上服务OOM,整个系统挂了!" 你心里一紧,赶紧登录服务器,发现日志里密密麻麻的java.lang.OutOfMemoryError: Java heap space
...
别慌!今天老司机就给你一套"OOM排查5连招",让你下次遇到这种情况,能淡定地说一句:"小场面,3分钟搞定!"
一、OOM的4种"死法",你中招的是哪种?
先搞清楚JVM有哪几种OOM,不同症状不同治法:
1. Java heap space - 堆内存爆了
症状:最常见的OOM,日志长这样:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.ArrayList.grow(ArrayList.java:267)
常见场景:
- 一次性加载大文件到内存
- 内存泄漏,对象无法被GC回收
- 集合类无限增长(如ArrayList疯狂add)
2. GC overhead limit exceeded - GC累死了
症状:JVM花了98%的时间在GC,但只回收了2%的内存:
java.lang.OutOfMemoryError: GC overhead limit exceeded
常见场景:
- 内存泄漏,GC根本回收不了多少内存
- 堆内存设置太小,对象创建速度超过回收速度
3. PermGen space / Metaspace - 方法区爆了
症状:Java 8之前是PermGen,之后是Metaspace:
// Java 7及之前
java.lang.OutOfMemoryError: PermGen space
// Java 8及之后
java.lang.OutOfMemoryError: Metaspace
常见场景:
- 动态生成类太多(如CGLIB代理)
- 类加载器泄漏
- 反射使用不当
4. Unable to create new native thread - 线程爆了
症状:无法创建新的本地线程:
java.lang.OutOfMemoryError: unable to create new native thread
常见场景:
- 线程池配置不合理,疯狂创建线程
- 系统线程数限制过低
二、5步排查法:从OOM到根因定位
第1步:确认OOM类型和发生位置
首先看日志,确认是哪种OOM,以及发生在哪个类:
# 查看异常日志
tail -f /var/log/app/error.log | grep -A 10 "OutOfMemoryError"
第2步:dump内存快照
OOM发生时,第一时间dump内存,这是破案的关键证据:
# 启动参数提前配置好,OOM时自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+UseCompressedOops
# 如果JVM还在运行,手动dump
jmap -dump:format=b,file=/tmp/heapdump.hprof <pid>
第3步:MAT分析内存快照
用MAT(Memory Analyzer Tool)分析dump文件,找出内存大户:
// 示例:找出占用内存最多的对象
Histogram histogram = queryHeap("SELECT * FROM java.lang.Object s");
for (ObjectHistogramRow row : histogram.getRows()) {
System.out.println(row.getLabel() + " " + row.getUsedHeapSize());
}
MAT三板斧:
- Histogram视图:看哪个类实例最多
- Dominator Tree:看哪个对象占用内存最大
- Path to GC Roots:找出为什么没被回收
第4步:代码审查+现场还原
结合MAT结果,定位到具体代码:
// 典型的内存泄漏案例
public class MemoryLeakDemo {
private static List<byte[]> leakList = new ArrayList<>();
public void addData(byte[] data) {
leakList.add(data); // 这里一直在add,从不remove!
}
}
第5步:验证修复效果
修复后,用压测验证:
# 使用JMeter压测,观察内存变化
# 启动参数增加监控
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/tmp/gc.log
三、实战案例:3个真实OOM惨案复盘
案例1:电商大促,订单服务堆内存OOM
背景:618大促,订单服务突然OOM,用户无法下单。
排查过程:
- 现象:日志显示
Java heap space
- dump分析:发现
HashMap$Node
实例占用了80%内存 - 定位代码:
// 问题代码:订单缓存无限增长
@Service
public class OrderCacheService {
private Map<Long, Order> orderCache = new ConcurrentHashMap<>();
public void cacheOrder(Order order) {
orderCache.put(order.getId(), order); // 只put不清理!
}
// 缺少清理逻辑
}
- 解决方案:
// 修复:增加LRU缓存,限制大小
@Service
public class OrderCacheService {
private Map<Long, Order> orderCache = Collections.synchronizedMap(
new LinkedHashMap<Long, Order>(1000, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<Long, Order> eldest) {
return size() > 1000; // 最多缓存1000个订单
}
}
);
// 或者使用Guava Cache
private Cache<Long, Order> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
}
效果:内存使用从90%降到30%,大促期间稳定运行。
案例2:报表系统,GC overhead limit exceeded
背景:财务系统生成月报表时,服务器直接卡死。
排查过程:
- 现象:
GC overhead limit exceeded
- dump分析:发现
StringBuilder
实例疯狂增长 - 定位代码:
// 问题代码:SQL拼接导致内存暴涨
public String buildReportSQL(List<Long> userIds) {
StringBuilder sql = new StringBuilder("SELECT * FROM orders WHERE user_id IN (");
for (Long userId : userIds) {
sql.append(userId).append(","); // 用户太多时,StringBuilder疯狂扩容
}
sql.append(")");
return sql.toString();
}
- 解决方案:
// 修复:分批处理,避免一次性加载
public List<ReportData> generateReport(List<Long> userIds) {
int batchSize = 1000;
List<ReportData> result = new ArrayList<>();
for (int i = 0; i < userIds.size(); i += batchSize) {
List<Long> batch = userIds.subList(i, Math.min(i + batchSize, userIds.size()));
result.addAll(processBatch(batch));
}
return result;
}
效果:报表生成时间从30分钟降到2分钟,内存使用稳定。
案例3:微服务网关,Metaspace OOM
背景:Spring Cloud网关运行一周后,频繁重启。
排查过程:
- 现象:
Metaspace
OOM - dump分析:发现大量
$Proxy
类实例 - 定位问题:动态代理类泄漏
根因:
- Spring Cloud Gateway使用CGLIB动态代理
- 每个路由都会生成新的代理类
- 类加载器泄漏导致类无法卸载
解决方案:
# 调整JVM参数
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
# Spring配置优化
spring:
cloud:
gateway:
discovery:
locator:
enabled: false # 关闭动态路由
效果:网关连续运行3个月无重启。
四、预防OOM的6个黄金法则
1. 合理设置JVM参数
# 生产环境推荐配置
-Xms4g -Xmx4g # 堆内存,生产环境建议Xms=Xmx
-Xmn2g # 新生代内存
-XX:SurvivorRatio=8
-XX:+UseG1GC # G1垃圾收集器
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
2. 代码审查重点
内存泄漏高危清单:
- 静态集合类(HashMap、ArrayList等)
- 单例模式持有大对象
- 数据库连接、文件流未关闭
- ThreadLocal使用不当
- 监听器、回调未注销
3. 监控告警体系
// JVM内存监控
@Component
public class JvmMemoryMonitor {
@Scheduled(fixedRate = 60000)
public void monitorMemory() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long used = heapUsage.getUsed();
long max = heapUsage.getMax();
double usage = (double) used / max;
if (usage > 0.8) {
// 内存使用率超过80%,发送告警
alertService.sendAlert("JVM内存使用率过高:" + usage * 100 + "%");
}
}
}
4. 压测验证
# 使用JMeter进行内存压测
# 重点关注:
# 1. 并发用户增加时内存变化
# 2. 长时间运行内存是否稳定
# 3. 大对象创建和销毁是否正常
# GC日志分析工具
java -jar gcviewer.jar gc.log
5. 代码规范
强制要求:
- 所有流操作必须使用try-with-resources
- 集合类必须指定初始容量
- 大对象使用后必须显式置null
- 避免在循环中创建大对象
// 正确示例
try (BufferedReader reader = new BufferedReader(new FileReader("large.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理逻辑
}
} catch (IOException e) {
log.error("文件读取失败", e);
}
// 避免内存泄漏
public class CacheManager {
private final Map<String, Object> cache = new WeakHashMap<>();
public void put(String key, Object value) {
cache.put(key, value); // 使用WeakHashMap避免内存泄漏
}
}
6. 上线前检查清单
发布前必查项:
- [ ] JVM参数是否合理
- [ ] 是否有内存泄漏风险代码
- [ ] 监控告警是否配置
- [ ] 压测是否通过
- [ ] 回滚方案是否准备
五、OOM急救包:5个命令行工具
1. jstat - 实时监控GC
jstat -gc <pid> 1000 # 每秒输出一次GC统计
2. jmap - 内存分析
jmap -histo <pid> # 查看对象统计
jmap -dump:format=b,file=heap.hprof <pid> # dump内存
3. jstack - 线程分析
jstack <pid> > thread.txt # 导出线程栈
4. jcmd - 多功能工具
jcmd <pid> GC.run # 强制GC
jcmd <pid> VM.flags # 查看JVM参数
5. VisualVM - 图形化工具
# 远程连接JVM进行实时监控
jvisualvm --jdkhome $JAVA_HOME
总结:OOM不可怕,可怕的是不会排查
JVM内存OOM问题,说到底就是3句话:
- 预防为主:合理设置JVM参数,规范代码
- 监控到位:早发现早处理,不要等用户投诉
- 工具熟练:MAT、jmap、jstat要会用
记住老司机的口诀:"一dump二分析三修复四验证",下次遇到OOM不慌!
附:OOM排查思维导图
OOM发生 → 确认类型 → dump内存 → MAT分析 → 定位代码 → 修复验证
↓ ↓ ↓ ↓ ↓ ↓
看日志 看参数 jmap命令 三板斧 代码审查 压测确认