Java 应用内存溢出 (OOM) 故障排查实战 一、OOM 概述 1.1 什么是 OOM 内存溢出(Out Of Memory,OOM)是 Java 应用最常见的故障之一,指 JVM 无法为新对象分配内存,且无法通过垃圾回收释放足够空间时抛出的错误。
1.2 OOM 的常见类型
错误类型
触发原因
常见场景
Java heap space
堆内存不足
对象泄漏、大对象、内存配置过小
GC overhead limit exceeded
GC 时间占比过高
内存接近耗尽,GC 频繁
Metaspace
元空间不足
类加载过多、动态代理、热部署
StackOverflowError
栈空间不足
递归过深、栈帧过大
Direct buffer memory
直接内存不足
NIO 使用、Netty 应用
Unable to create new native thread
无法创建线程
线程数过多、系统资源限制
Out of swap space
系统交换空间不足
物理内存 + 交换空间耗尽
Killed process
被 OOM Killer 杀死
系统级内存不足
二、故障现象识别 2.1 应用层症状 1 2 3 4 5 6 7 8 9 10 # 应用日志中出现 java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: GC overhead limit exceeded java.lang.OutOfMemoryError: Metaspace # 应用表现 - 响应时间突然变长 - 请求超时增加 - 服务间歇性不可用 - 进程突然消失(被 OOM Killer 杀死)
2.2 系统层症状 1 2 3 4 5 6 7 8 9 10 11 12 ps aux | grep java free -h dmesg | grep -i "killed process" grep -i "out of memory" /var/log/messages top -p <pid>
2.3 典型 OOM Killer 日志 1 2 [12345.678901] Out of memory: Kill process 12345 (java) score 900 or sacrifice child [12345.678902] Killed process 12345 (java) total-vm:8234567kB, anon-rss:7123456kB, file-rss:12345kB
三、诊断工具准备 3.1 内置工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 jcmd <pid> help jcmd <pid> GC.heap_info jcmd <pid> VM.flags jmap -heap <pid> jmap -histo <pid> jmap -dump:format=b,file=heap.hprof <pid> jstat -gc <pid> 1000 10 jstat -gcutil <pid> 1000 10 jstack <pid> > thread_dump.txt jinfo -flags <pid> jinfo -sysprops <pid>
3.2 第三方工具
工具
用途
特点
MAT (Memory Analyzer Tool)
堆转储分析
Eclipse 出品,功能强大
JVisualVM
实时监控
JDK 自带,图形化
Arthas
在线诊断
阿里巴巴开源,无需重启
GCEasy
GC 日志分析
在线工具,可视化好
Async Profiler
性能分析
低开销,支持火焰图
3.3 工具安装 1 2 3 4 5 6 7 wget https://www.eclipse.org/downloads/download.php?file=/mat/1.12.0/rcp/MemoryAnalyzer-1.12.0-rcp-linux.gtk.x86_64.tar.gz tar -xzf MemoryAnalyzer-*.tar.gz wget https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar
四、排查流程 4.1 标准排查流程图 1 2 3 4 5 发现 OOM → 收集现场 → 分析原因 → 定位代码 → 修复验证 ↓ ↓ ↓ ↓ ↓ 监控告警 堆转储 MAT 分析 源码审查 压测验证 GC 日志 线程分析 日志排查 灰度发布 系统日志 火焰图 复现测试 全量上线
4.2 第一步:保护现场 关键原则:先保留现场,再重启恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jmap -dump:live,format=b,file=/tmp/heap_$(date +%Y%m%d_%H%M%S).hprof <pid> jstack <pid> > /tmp/thread_$(date +%Y%m%d_%H%M%S).txt jinfo <pid> > /tmp/jinfo_$(date +%Y%m%d_%H%M%S).txt top -b -n 1 > /tmp/top_$(date +%Y%m%d_%H%M%S).txt free -h > /tmp/memory_$(date +%Y%m%d_%H%M%S).txt
4.3 第二步:分析堆转储 使用 MAT 分析:
打开 MAT,加载 heap.hprof 文件
查看 “Leak Suspects” 报告
分析 “Dominator Tree” 找出大对象
查看 “Histogram” 统计对象数量
关键指标:
1 2 3 - Shallow Heap: 对象自身占用内存 - Retained Heap: 对象及其引用对象总内存 - GC Roots: 对象引用链起点
常见泄漏模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class CacheManager { private static Map<String, Object> cache = new HashMap <>(); public void put (String key, Object value) { cache.put(key, value); } } public void processData () throws Exception { InputStream is = new FileInputStream ("large_file" ); byte [] data = IOUtils.toByteArray(is); } public class EventListener { public void register () { eventBus.register(this ); } } public class UserContext { private static ThreadLocal<User> user = new ThreadLocal <>(); public void setUser (User u) { user.set(u); } }
4.4 第三步:分析 GC 日志 启用 GC 日志:
1 2 3 4 5 6 7 8 9 10 11 12 -Xloggc:/var/log/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -Xlog:gc*:file=/var/log/gc.log:time ,uptime ,level,tags -Xlog:gc*:file=/var/log/gc.log:time ,uptime ,level,tags:filecount=10,filesize=100M
分析要点:
1 2 3 4 5 6 7 8 1. GC 频率是否过高(每分钟超过 1 次 Full GC) 2. GC 耗时是否过长(单次 Full GC 超过 1 秒) 3. 老年代使用率是否持续增长 4. GC 后内存是否有效回收
典型 GC 问题日志:
1 2 3 4 5 6 7 8 9 10 # Full GC 频繁 2026-03-14T15:30:01.123+0800: [Full GC (Ergonomics) [PSYoungGen: 1024K->0K(76288K)] [ParOldGen: 71680K->71500K(175616K)] 72704K->71500K(251904K), 1.234 secs] # GC 时间占比过高 2026-03-14T15:30:05.456+0800: [GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76288K)] 137216K->79872K(251904K), 0.567 secs] # 触发 GC overhead limit java.lang.OutOfMemoryError: GC overhead limit exceeded
4.5 第四步:使用 Arthas 在线诊断 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 java -jar arthas-boot.jar dashboard memory vmtool --action getInstances --className java.lang.String --limit 10 vmtool --action getInstances --className com.example.MyClass --limit 5 trace com.example.Service process '#cost > 100' thread thread -n 5
五、典型场景案例分析 5.1 案例一:缓存无限增长 现象 :
应用运行 3 天后开始频繁 Full GC
第 5 天出现 OOM 崩溃
排查过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 jmap -dump:live,format=b,file=heap.hprof <pid> - HashMap 占用 4GB,占总堆 80% - HashMap 中有 500 万个 Entry - 键为 String,值为自定义对象 public class ProductCache { private static Map<String, Product> cache = new HashMap<>(); public void cacheProduct(String productId, Product product) { cache.put(productId, product); // 无过期、无上限 } }
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static Map<String, Product> cache = new LinkedHashMap <String, Product>(10000 , 0.75f , true ) { @Override protected boolean removeEldestEntry (Map.Entry<String, Product> eldest) { return size() > 10000 ; } }; private static Cache<String, Product> cache = CacheBuilder.newBuilder() .maximumSize(10000 ) .expireAfterWrite(1 , TimeUnit.HOURS) .build(); private static Cache<String, Product> cache = Caffeine.newBuilder() .maximumSize(10000 ) .expireAfterWrite(1 , TimeUnit.HOURS) .recordStats() .build();
5.2 案例二:大对象直接加载 现象 :
导入 Excel 文件时偶发 OOM
小文件正常,大文件(>100MB)必现
排查过程 :
1 2 3 4 5 6 7 8 public void importExcel (MultipartFile file) throws Exception { byte [] data = file.getBytes(); Workbook workbook = new XSSFWorkbook (new ByteArrayInputStream (data)); }
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void importExcel (MultipartFile file) throws Exception { try (InputStream is = file.getInputStream(); OPCPackage pkg = OPCPackage.open(is)) { XSSFReader reader = new XSSFReader (pkg); ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable (pkg); XMLReader sheetParser = XMLHelper.getXMLReader(); sheetParser.setContentHandler(new XSSFSheetXMLHandler (...)); sheetParser.parse(new InputSource (reader.getSheet("Sheet1" ))); } } public void importExcel (MultipartFile file) { EasyExcel.read(file.getInputStream(), DataModel.class, new ReadListener () { @Override public void invoke (DataModel data, AnalysisContext context) { processRow(data); } }).sheet().doRead(); }
5.3 案例三:ThreadLocal 内存泄漏 现象 :
Tomcat 应用运行一段时间后 OOM
重启后恢复正常
排查过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class UserContextHolder { private static ThreadLocal<User> currentUser = new ThreadLocal <>(); public static void setUser (User user) { currentUser.set(user); } public static User getUser () { return currentUser.get(); } } public class UserController { public void handleRequest () { User user = authenticate(); UserContextHolder.setUser(user); } }
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class UserContextHolder { private static ThreadLocal<User> currentUser = new ThreadLocal <>(); public static void setUser (User user) { currentUser.set(user); } public static User getUser () { return currentUser.get(); } public static void clear () { currentUser.remove(); } } public class UserContextInterceptor implements HandlerInterceptor { @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContextHolder.clear(); } } public void handleRequest () { try { User user = authenticate(); UserContextHolder.setUser(user); } finally { UserContextHolder.clear(); } }
5.4 案例四:元空间溢出 现象 :
1 java.lang.OutOfMemoryError: Metaspace
原因分析 :
动态生成类过多(CGLIB、Groovy)
热部署频繁(DevTools、JRebel)
类加载器泄漏
排查命令 :
1 2 3 4 5 6 7 8 jcmd <pid> VM.metaspace jcmd <pid> GC.class_histogram | head -50 jcmd <pid> GC.classloader_stats
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m // 避免重复创建代理类 private static final Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>(); public Object getProxy(Class<?> clazz) { return proxyCache.computeIfAbsent(clazz, c -> createProxy(c)); }
5.5 案例五:被 OOM Killer 杀死 现象 :
进程突然消失,无 OOM 错误日志
系统日志显示被 kill
排查 :
1 2 3 4 5 6 7 8 dmesg -T | grep -i "killed process" grep -i "out of memory" /var/log/messages
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -Xms8g -Xmx8g echo -500 > /proc/<pid>/oom_score_adjsudo fallocate -l 8G /swapfilesudo chmod 600 /swapfilesudo mkswap /swapfilesudo swapon /swapfiledocker run -m 8g --memory-swap=10g ... resources: requests: memory: "8Gi" limits: memory: "10Gi"
六、预防措施 6.1 JVM 参数优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 java -Xms8g -Xmx8g \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=512m \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:InitiatingHeapOccupancyPercent=45 \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/log/heapdump \ -Xlog:gc*:file=/var/log/gc.log:time ,uptime ,level,tags:filecount=10,filesize=100M \ -XX:+UseGCLogFileRotation \ -XX:NumberOfGCLogFiles=10 \ -XX:GCLogFileSize=100M \ -jar application.jar
6.2 代码规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private static List<Data> dataList = new ArrayList <>();private static final int MAX_SIZE = 10000 ;private static BlockingQueue<Data> dataQueue = new ArrayBlockingQueue <>(MAX_SIZE);try (InputStream is = new FileInputStream (file); BufferedReader reader = new BufferedReader (new InputStreamReader (is))) { } public void processLargeData (List<Data> data) { int batchSize = 1000 ; for (int i = 0 ; i < data.size(); i += batchSize) { int end = Math.min(i + batchSize, data.size()); processBatch(data.subList(i, end)); } } private static Map<String, SoftReference<CacheEntry>> cache = new ConcurrentHashMap <>(); private static final ThreadLocal<DateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat ("yyyy-MM-dd" )); dateFormat.remove();
6.3 监控告警 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 groups: - name: jvm-alerts rules: - alert: JvmHeapUsageHigh expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85 for: 5m labels: severity: warning annotations: summary: "JVM 堆内存使用率超过 85%" - alert: JvmFullGcFrequent expr: rate(jvm_gc_collection_seconds_count{gc="G1 Old Generation"}[5m]) > 0.5 for: 5m labels: severity: warning annotations: summary: "Full GC 过于频繁" - alert: JvmGcTimeRatioHigh expr: rate(jvm_gc_collection_seconds_sum[5m]) / 300 > 0.1 for: 5m labels: severity: critical annotations: summary: "GC 时间占比超过 10%"
6.4 压测验证
七、应急响应清单 7.1 故障发生时的操作 1 2 3 4 5 6 □ 1. 确认故障现象(查看日志、监控) □ 2. 保留现场(堆转储、线程转储、GC 日志) □ 3. 评估影响范围(受影响服务、用户) □ 4. 临时恢复(重启服务,如有必要) □ 5. 通知相关人员(开发、测试、业务方) □ 6. 记录故障时间线
7.2 恢复后的操作 1 2 3 4 5 6 7 8 □ 1. 分析堆转储文件 □ 2. 定位根本原因 □ 3. 制定修复方案 □ 4. 代码修复 + 单元测试 □ 5. 压测验证 □ 6. 灰度发布 □ 7. 全量上线 □ 8. 故障复盘 + 文档沉淀
八、总结 OOM 故障排查的核心要点:
保护现场优先 :先 dump 堆,再重启恢复
工具链完备 :熟练掌握 jmap、MAT、Arthas 等工具
分析有方法 :从堆转储→GC 日志→代码定位
预防胜于治疗 :合理配置 JVM 参数 + 代码规范 + 监控告警
持续优化 :定期压测 + 代码审查 + 故障复盘
记住:每一次 OOM 故障都是优化系统的机会,做好记录和复盘,避免同类问题再次发生。