OOM问题有这几种:
一、理解OOM的类型
首先,你需要知道OOM有不同的类型,这能为你指明排查方向。常见的OOM错误信息有:
java.lang.OutOfMemoryError: Java heap space
含义: Java堆内存不足,无法分配对象。这是最常见的一种。
原因: 内存泄漏或堆空间设置过小。
java.lang.OutOfMemoryError: Metaspace (Java 8+) / PermGen space (Java 7及以前)
含义: 元空间(用于存类元信息、方法区数据)不足。
原因: 动态生成了大量类(如CGLib、JSP)、应用部署了大量应用、元空间设置过小。
java.lang.OutOfMemoryError: Unable to create new native thread
含义: 无法创建新的本地线程。
原因: 创建的线程数超过系统限制(如Linux的ulimit)、内存不足、线程泄漏。
java.lang.OutOfMemoryError: GC overhead limit exceeded
含义: GC花费了太多时间(默认98%以上)但只回收了非常少的内存(默认2%)。
原因: 通常是堆内存太小或存在内存泄漏,导致GC效率极低。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
含义: 尝试分配一个超过虚拟机限制的数组。
原因: 应用程序错误,如尝试分配 Integer.MAX_VALUE 大小的数组。
二、排查步骤与工具
当发生OOM时,不要慌张,请遵循以下步骤。
第1步:保留现场(至关重要!)
一旦发生OOM,JVM可能就会退出。为了能事后分析,必须在启动JVM时加上以下参数:
bash
# 堆内存OOM相关-XX:+HeapDumpOnOutOfMemoryError# 在发生OOM时自动生成堆转储文件-XX:HeapDumpPath=/path/to/dumps/# 指定堆转储文件的保存路径-XX:OnOutOfMemoryError="your-cmd"# 发生OOM时执行一个脚本,可用于告警或重启# GC日志相关-Xloggc:/path/to/gc.log# 输出GC日志文件-XX:+PrintGCDetails# 打印GC详情-XX:+PrintGCDateStamps# 打印GC时间戳-XX:+PrintGCTimeStamps# 打印GC相对时间戳-XX:+UseGCLogFileRotation# GC日志滚动-XX:NumberOfGCLogFiles=5# 保留5个日志文件-XX:GCLogFileSize=10M# 每个日志文件10M
有了 HeapDump 文件,你就有了案发现场的“完整快照”。
第2步:分析堆转储文件
使用专业的工具来分析上一步生成的 .hprof 文件。
Eclipse MAT (Memory Analyzer Tool): 强烈推荐,功能强大,能自动分析泄漏嫌疑。
JVisualVM: JDK自带,功能相对基础,但方便快捷。
JProfiler / YourKit: 商业级性能分析工具,非常强大。
使用MAT分析的一般流程:
打开 .hprof 文件。
查看 Leak Suspects Report(泄漏嫌疑报告),MAT会自动给出可能发生内存泄漏的对象和引用链。
查看 Histogram(直方图),可以看到每个类的实例数量和总占用内存。按 Retained Heap(支配内存)排序,找到占用最大的类。
对可疑的类,右键 -> Merge Shortest Paths to GC Roots -> with all references,查看到GC Roots的完整引用链,从而定位为什么这些对象无法被回收。
第3步:分析GC日志
使用GC日志分析工具来查看GC的详细情况。
gceasy.io: 在线分析工具,上传GC日志即可得到非常直观的分析报告。
GCViewer: 一个开源的离线GC日志分析工具。
关注点:
Full GC的频率和持续时间: 是否过于频繁?每次暂停时间是否过长?
内存回收效果: 每次GC后,老年代/堆的使用率是否明显下降?如果下降不多,说明可能存在内存泄漏。
GC原因: 是什么原因触发了GC?
第4步:实时监控(针对线上系统)
对于线上系统,除了事后分析,还需要实时监控。
JDK自带工具:
jps: 查看Java进程ID。
jstat -gc <pid> <interval>: 实时查看GC统计信息,如 jstat -gc 12345 1s。
jmap -heap <pid>: 查看堆概要信息。
jmap -histo:live <pid>: 查看堆中对象的直方图(不要轻易在生产环境执行,会触发Full GC)。
可视化APM/监控工具:
Prometheus + Grafana: 通过JMX Exporter暴露JVM指标,进行采集和可视化。
Arthas: 阿里开源的Java诊断神器,可以在不重启应用的情况下进行动态诊断。常用命令:
dashboard: 整体系统监控面板。
heapdump: 动态生成堆转储。
jvm: 查看JVM信息。
thread: 查看线程信息。
monitor / watch / trace: 方法执行监控。
三、解决方案
根据排查出的根本原因,采取相应的解决方案。
针对 Java heap space
内存泄漏:
通过MAT分析引用链,找到持有该对象的“罪魁祸首”(例如,一个静态的Map、未关闭的连接、线程池等)。
修复代码,解除不必要的强引用,特别是来自静态集合和GC Roots的引用。
确保使用 try-with-resources 或 finally 块关闭所有资源(如文件流、数据库连接)。
堆大小不足:
适当调大堆内存: -Xmx4g(设置最大堆为4GB)。但不要盲目调大,要结合系统总内存。
优化程序,减少不必要的对象创建,使用对象池(需谨慎,可能增加复杂度)。
GC策略不当:
对于大内存、多核服务器,可以考虑使用G1 GC (-XX:+UseG1GC) 或 ZGC (-XX:+UseZGC)。它们通常有更低的停顿时间和更好的吞吐量。
针对 Metaspace
调大元空间:
-XX:MaxMetaspaceSize=256m(设置元空间最大容量)。
排查类加载问题:
检查是否有框架(如Spring)在动态生成大量代理类。
检查是否有Web应用重复部署导致类未卸载。
使用 -XX:+TraceClassLoading 和 -XX:+TraceClassUnloading 来跟踪类的加载和卸载。
针对 Unable to create new native thread
减少线程创建: 优化代码,使用线程池,避免无限制地创建线程。
调整系统限制: 检查并调整操作系统对用户进程的线程数限制 (ulimit -u)。
减少线程栈大小: 如果线程不需要很深的栈,可以减小线程栈大小 -Xss256k(默认通常是1MB)。
针对 GC overhead limit exceeded
此错误通常是堆OOM的前兆,解决方案与 Java heap space 类似,核心是解决内存泄漏或调整堆/GC参数。
总结:排查流程图
记住,解决OOM问题的关键在于 “保留现场 -> 分析数据 -> 定位根源 -> 验证修复”。熟练掌握MAT和GC日志分析工具,是每个Java高级工程师的必备技能。
