大家好,今天咱们聊一个让无数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网关运行一周后,频繁重启。
排查过程:
- 现象:
MetaspaceOOM - 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命令   三板斧   代码审查   压测确认









