Java应用OOM问题排查

更新时间:
复制为 MD 格式

本文介绍Java应用OOM问题的排查思路。

Java内存模型简介

JVM内存结构概览

Java虚拟机的内存主要分为以下几个区域:

  • 堆内存(Heap):存储Java对象实例,是垃圾回收的主要区域。

    • 年轻代(Young Generation):Eden区 + Survivor区。

    • 老年代(Old Generation):长期存活的对象。

  • 堆外内存(Off-Heap)

    • 元空间(Metaspace):存储类的元数据信息。

    • 压缩类空间(Compressed Class Space):与压缩指针相关。

    • 代码缓冲区(Code Cache):JIT编译后的本地代码。

    • 直接内存(Direct Memory):NIO操作使用的缓冲区。

  • 栈内存(Stack)

    • 虚拟机线程栈:存储方法调用的局部变量和操作数栈。

    • 本地方法栈:JNI调用使用。

说明

深入了解JVM内存监控的详细信息,请参考:JVM监控内存详情说明

定位OOM内存区域

通过异常日志精确定位OOM来源

这是最准确、最直接的定位方法。当发生OOM时,JVM通常会在日志中输出具体的异常信息,通过这些信息可以直接确定是哪个内存区域出现问题。

步骤1:确定日志位置

根据不同的部署方式,OOM日志可能出现在以下位置:

  • 物理机/虚拟机部署

    • 应用日志文件:/path/to/app/logs/app.logserver.log等。

    • 标准输出/错误:nohup.out、控制台输出。

    • 系统日志:/var/log/messages(查看是否被系统OOM Killer杀死)。

  • 容器/K8s部署

    # 查看当前容器日志
    kubectl logs <pod-name> -c <container-name>
      
    # 如果Pod已重启,查看上一次的日志
    kubectl logs <pod-name> -c <container-name> --previous
      
    # Docker环境
    docker logs <container-id>

步骤2:搜索OOM相关日志

# 在日志文件中搜索OutOfMemoryError
grep -i "OutOfMemoryError" /path/to/logs/*.log

# 搜索StackOverflowError
grep -i "StackOverflowError" /path/to/logs/*.log

# 如果日志被压缩
zgrep -i "OutOfMemoryError" /path/to/logs/*.log.gz

# 查看系统是否杀死了Java进程
dmesg | grep -i "killed process"
grep -i "java" /var/log/messages | grep -i "killed"

异常信息对应的内存区域

异常信息

内存区域

下一步排查方向

java.lang.OutOfMemoryError: Java heap space

堆内存

堆内存问题排查

java.lang.OutOfMemoryError: Metaspace

元空间

元空间(Metaspace)问题排查

java.lang.OutOfMemoryError: Direct buffer memory

直接内存

直接内存(Direct Memory)问题排查

java.lang.OutOfMemoryError: unable to create new native thread

线程栈/系统资源

其他堆外内存区域定位与排查

重要

如果能找到明确的OOM异常日志,基本可以100%确定问题区域,这比任何监控工具都要准确。

通过应用监控定位(日志不全时的补充手段)

当无法获取到完整的OOM异常日志时(如日志被覆盖、进程突然消失等),可以通过应用监控来分析各内存区域的使用趋势,推断可能的OOM来源。

查看JVM内存监控曲线

在控制台查看目标应用的实例监控页面,重点观察以下几个关键指标:

堆内存使用趋势

  • 正常情况:堆使用呈锯齿状,GC后明显回落。

  • 堆内存不足的信号

    • 堆使用率长期在80%以上。

    • Full GC频繁但老年代回收效果差。

    • 堆使用曲线接近最大堆内存上限。

  • 堆内存泄漏的信号

    • 老年代使用量单调递增。

    • Full GC后的"谷底"越来越高。

    • 重启后又重复相同的增长模式。

非堆内存使用趋势

  • 正常情况:内存使用相对稳定,偶有小幅增长。

  • 问题的信号

    • 例如相关内存空间使用量持续上升,比如元空间接近或达到MaxMetaspaceSize限制。

    • Full GC无法有效回收相关内存空间。

通过GC事件和GC Cause分析

在控制台查看目标应用的实例监控页面,单击目标实例的JVM监控,应用监控采集了详细的GC事件,包含GC触发原因(GC Cause),这对定位OOM来源有帮助:

常见跟OOM相关GC Cause及其含义

GC Cause

含义

可能的内存问题

排查建议

Allocation Failure

堆空间分配失败,无法为新对象分配内存

堆内存压力大

优先排查堆内存问题,检查是否需要增大堆或存在内存泄漏

G1 Evacuation Pause (Allocation Failure)

G1收集器中的分配失败

堆内存压力大

同上,G1收集器特有的表现形式

Metadata GC Threshold

元空间使用达到GC阈值

元空间压力大

检查类加载情况,可能存在动态类生成或类加载泄漏

GCLocker Initiated GC

GC锁定后触发的GC

JNI/直接内存相关

可能与JNI调用或直接内存使用相关,检查native代码调用

CodeCache GC Threshold

代码缓存达到阈值

代码缓存不足

JIT编译的代码过多,可能需要增大CodeCache

分析方法

  1. 查看OOM发生前5分钟的GC事件。

  2. 统计各种GC Cause的出现频率。

  3. 结合内存使用曲线判断主要压力来源。

堆内存问题排查

当确认OOM来源是堆内存(java.lang.OutOfMemoryError: Java heap space)后,需要进一步分析具体原因。堆内存OOM主要有两种情况:配置不足内存使用不合理

问题分类判断

首先通过以下几个问题来判断属于哪种情况:

  • 时间维度分析

    这个OOM问题是什么时候开始出现的?

    最近才开始出现

    • 检查近期是否有业务变化(流量增长、新功能上线、大促活动等)。

    • 检查近期是否有技术变化(JVM参数调整、框架升级等)。

    • 如果有明显的业务增长,优先考虑配置不足。

    一直存在,但越来越频繁

    • 通常是内存使用不合理导致的渐进式问题。

    • 需要通过内存分析工具定位具体原因。

  • 业务流量维度分析

    近期业务流量是否有显著变化?

    通过ARMS应用监控或其他APM工具查看:

    • QPS/TPS是否有明显增长。

    • 响应时间是否变长。

    • 请求体/响应体大小是否增加。

    判断标准

    • 如果流量增长50%以上,且OOM时间与流量高峰重合 → 配置不足。

    • 如果流量基本稳定,但OOM越来越频繁 → 内存使用不合理。

  • ARMS监控曲线分析

    堆内存使用曲线是什么模式?

    锯齿形但峰值越来越高:Full GC能回收内存,但高峰期经常触顶,通常是配置不足。

    单调递增,Full GC效果越来越差:老年代基线不断上升通常是内存泄漏或使用不合理。

堆内存配置不足

典型特征

  • 业务流量或数据量近期有明显增长。

  • ARMS监控显示堆使用率长期在80%以上。

  • Full GC后能释放内存,但高峰期容易触顶。

  • GC频率增加,但单次GC效果还算正常。

解决方案

步骤1:评估当前配置

# 查看当前JVM参数
ps -ef | grep java | grep -E "Xmx|Xms"

# 查看机器/容器资源
free -h  # 物理机
kubectl describe pod <pod-name>  # K8s环境

步骤2:参考配置建议

下表给出常见机器规格下的推荐 JVM 内存参数,仅作为通用服务的参考基线,实际使用时仍需要结合业务特性和监控数据进行微调(建议线上配置前请在测试环境中进行充分测试验证):

JVM参数

说明

1c2g

2c4g

4c8g

8c16g

-Xms

初始堆内存大小

1g

2560m

4g

10g

-Xmx

最大堆内存大小

1g

2560m

4g

10g

-Xmn

新生代空间大小

500m

1200m

2g

5g

-Xss

线程堆栈空间大小(JDK8 默认 1M)

1m

1m

1m

1m

-XX:MetaspaceSize

初始元空间大小

128m

256m

384m

512m

-XX:MaxMetaspaceSize

最大元空间大小

128m

256m

384m

512m

-XX:MaxDirectMemorySize

最大堆外(Direct)内存大小

256m

256m

1g

1g

-XX:ReservedCodeCacheSize

Code Cache 大小

64m

128m

256m

256m

使用建议:

  • 上表假设 Java 进程是该机器/容器上的主要进程,并预留部分内存给操作系统和 Page Cache。

  • 如业务大量依赖堆外内存(Netty、NIO、大量压缩/解压等),可以适当提高 -XX:MaxDirectMemorySize,同时相应降低堆或确保整体不超过物理内存/容器限制。

  • 上线后务必结合 ARMS 等监控观察堆、元空间、直接内存曲线,根据实际运行情况再做细化调优。

步骤3:验证效果

  • 调整后观察ARMS监控,堆使用率应降到70%以下。

  • GC频率和耗时应有明显改善。

  • 业务高峰期不再频繁触发Full GC。

内存使用不合理(代码问题)

典型特征

  • 业务流量基本稳定,但OOM越来越频繁。

  • 老年代使用量单调递增。

  • Full GC后的内存"谷底"越来越高。

  • 重启后又重复相同的增长模式。

排查步骤

步骤1:创建内存快照

开启OOM时自动生成堆转储:

# 在JVM启动参数中添加,容器环境需要对/tmp/目录做持久化,避免pod重启导致快照数据丢失
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof

或者可以在内存高水位时通过内存快照白屏或者以下命令黑屏生成堆转储:

# 当ARMS显示堆使用率超过80%时执行
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

步骤2:堆转储文件分析

使用应用诊断分析平台ATPEclipse MAT分析堆转储文件:

  1. 查看内存占用Top N的对象类型

    • 关注占用内存最多的类(通常是集合类、缓存类、业务对象等)。

    • 分析这些对象是否合理。

  2. 分析对象引用链

    • 查看大对象被谁持有(GC Roots路径分析)。

    • 确认是否应该被回收但被意外持有。

堆外内存问题排查

堆外内存OOM现象或原因:

  • 进程实际内存占用远超JVM堆设置。

  • 系统内存不足,可能触发OOM Killer。

元空间(Metaspace)问题排查

问题分类判断

先判断是配置问题还是泄露问题

  • 配置不足的特征

    • 应用启动后元空间使用量相对稳定。

    • 在特定操作(如大量类加载)后达到上限。

    • 重启后元空间使用量恢复正常水平。

  • 元空间泄漏的特征

    • 元空间使用量持续单调上升。

    • Full GC无法有效回收元空间。

    • 类加载数量异常增长。

元空间配置不足

现象识别

# 查看元空间使用情况
jstat -gc <pid>
# 关注MC(元空间容量)和MU(元空间使用量)

# 查看类加载情况  
jstat -class <pid>

解决方案

# 适当增大元空间配置
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

# 注意:MetaspaceSize是初始阈值,MaxMetaspaceSize是硬限制
# 建议生产环境必须设置MaxMetaspaceSize,避免无限制使用物理内存

元空间泄露(动态类生成问题)

  1. 分析类加载趋势

    # 查看类加载统计(按加载数量排序)
    jcmd <pid> GC.class_stats | head -20
       
    # 持续监控类加载变化
    jstat -class <pid> 1000 10
       
    # 开启类加载日志(调试环境)
    -verbose:class
  2. 定位问题根因以下为常见元空间泄漏场景

    • FastJSON动态类生成

      • 现象:大量com.alibaba.fastjson.asm相关的类。

      • 解决:升级FastJSON版本或替换为Jackson。

    • 动态代理泄露

      • 现象:大量$ProxyCGLIB$$类。

      • 解决:检查Spring AOP、动态代理的使用方式。

    • Groovy脚本引擎

      • 现象:大量动态编译的Groovy类。

      • 解决:重用ScriptEngine实例,避免频繁创建。

    • Bean拷贝工具

      • 现象:大量BeanCopierDelegatingClassLoader

      • 解决:避免使用Apache BeanUtils,推荐使用MapStruct。

  3. 使用堆转储分析

    # 生成堆转储
    jmap -dump:format=b,file=metaspace.hprof <pid>
       
    # 在ATP/MAT中重点查看:
    # - ClassLoader实例数量
    # - Class对象数量  
    # - 是否有大量无法回收的ClassLoader

直接内存(Direct Memory)问题排查

问题分类判断

判断是配置问题还是泄漏问题

  • 配置不足的特征

    • 业务高峰期出现Direct buffer memory OOM。

    • NIO/Netty使用量确实很大。

    • 重启后问题暂时缓解。

  • 直接内存泄漏的特征

    • 进程RES内存持续增长,远超JVM堆设置。

    • NMT显示Direct Memory部分持续上升。

    • 即使业务量不大也会逐渐OOM。

现象识别和初步分析

# 1. 对比进程实际内存和JVM堆设置
top -p <pid>  # 查看RES
ps -ef | grep java | grep Xmx  # 查看堆设置

# 2. 使用NMT查看直接内存使用
-XX:NativeMemoryTracking=detail
jcmd <pid> VM.native_memory detail | grep -A5 "Direct"

# 3. 查看直接内存配置
ps -ef | grep java | grep MaxDirectMemorySize

其他堆外内存区域定位与排查

针对其他堆外内存区域的定位和排查,可以尝试使用阿里云操作系统团队提供的内存全景分析工具进行排查。