V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
superhot
V2EX  ›  程序员

请教 Java OOM 及 JVM 相关的问题

  •  
  •   superhot · 41 天前 · 2706 次点击
    这是一个创建于 41 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    1. AWS EC2 t3.medium 实例,Amazon Linux 2 系统,4GB 内存。
    2. Java 启动时 Heap 的配置为 -Xmx2847m,大概是给其他服务留 1G 左右内存,其余全部分配给 JVM 。
    3. 结合日志和后台监控发现频繁出现 OOM 导致 Tomcat 重启的问题。

    问题

    1. 现在每个实例的平均内存使用率在 93% 左右,此现象是否正常?
    2. 保持当前 EC2 实例配置不变的情况下,给 JVM Heap 分配多大内存比较合适?有什么可以拿来当作判断的依据吗?
    3. 除了 Heap 之外,JVM 还有 Metaspace 、CodeCache 、DirectByteBuffers 等等,这些 Heap 之外的部分可能吃掉多少内存?有什么可以拿来当作判断的依据吗?
    4. 如何分析 OOM 可能的情况?/usr/share/tomcat 目录下有个 hs_err_pid 前缀的日志文件,似乎在 OOM 时会输出相关错误信息,但根本看不懂……

    一年后端经验的 CRUD Boy 没系统学过 Java ,不了解 JVM ,突然让去解决 OOM 的问题,实在懵逼,不知从何下手,请各位 Java 大佬们给点建议,救救本菜,谢谢大家!

    第 1 条附言  ·  41 天前

    使用的是 Java 11,hs_err_pid 的内容在这里

    43 条回复    2024-10-18 16:02:12 +08:00
    BBCCBB
        1
    BBCCBB  
       41 天前
    堆内内存的话, dump 一份内存下来看占用内存的都是什么东西.
    2Nfree
        2
    2Nfree  
       41 天前
    可以用 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump 导出 OOM 时的 heap dump ,然后用工具分析,或者直接在应用程序运行的时候使用 jmap 导出当前对象分布的情况进行分析,推荐用这个: https://arthas.aliyun.com/doc/
    INCerry
        3
    INCerry  
       41 天前
    什么版本的 Java ?如果版本高只想解决问题,那先:

    -XX:+UnlockExperimentalVMOptions
    -XX:+UseShenandoahGC
    -XX:ShenandoahGCHeuristics=compact
    Geekerstar
        4
    Geekerstar  
       41 天前
    把这个 hs_err_pid 日志发出来,我最近也遇到 OOM 了,能帮忙排查下。可以看我最近发的一个帖子。
    defunct9
        5
    defunct9  
       41 天前
    升级 ec2 ,给 8G 内存
    wenning
        6
    wenning  
       41 天前
    springboot 的话加上 actuator, 配合 prometheus, grafana 看看 jvm 相关的使用情况; 90%的占用肯定不正常了, 相当于比较吃紧了, 如果来点压力就挂了, 再留一点内存给系统, 否则内存满了直接会被系统 kill
    ZZ74
        7
    ZZ74  
       41 天前
    现在每个实例的平均内存使用率在 93% 左右,此现象是否正常? 正常。
    参考这个帖子 https://www.v2ex.com/t/1078482?p=1#reply22 贴出 hs_err_pid 内容。贴给 AI 都会给你分析。
    xiwolaisi
        8
    xiwolaisi  
       41 天前
    jvm dump 分析网站,我自己懒得跑 MAT 的时候就用这个👻
    https://heaphero.io/heap-index.jsp#header
    jorneyr
        9
    jorneyr  
       41 天前
    例如代码里把 1G 文件全部一次性读入内存,先分析代码的原因。
    paranoiagu
        10
    paranoiagu  
       41 天前 via Android
    给太高了吧。
    paranoiagu
        11
    paranoiagu  
       41 天前 via Android
    除了堆内存还有堆外内存。
    superhot
        12
    superhot  
    OP
       41 天前
    @INCerry Java11 ,应该不算很高

    @Geekerstar 链接贴在附言里了,函数名修改过,先谢过老哥,我去帖子学习下

    @defunct9 只有升配置加内存这一条路了吗。。7 80 个实例,有点顶不住 ; w ;

    @ZZ74 问过 copilot ,没什么干货

    @wenning 我也有点怀疑这个


    @xiwolaisi
    @BBCCBB
    @2Nfree
    感谢提供思路,只是听过名词,对这些还没什么清晰的认识,我再研究研究
    sagaxu
        13
    sagaxu  
       41 天前
    -Xmx 调小一点试试,比如 2000m

    总内存只有 4G ,设置-Xmx2847m 一定会 OOM
    yuemingming
        14
    yuemingming  
       41 天前   ❤️ 1
    「结合日志和后台监控发现频繁出现 OOM 导致 Tomcat 重启的问题。」
    贴一下后台异常重启的日志呢。
    正常来说一个系统平稳运行是不应该 OOM 的。即使你的堆内存比较小,但是只要系统使用了内存之后正常释放,也是可以通过垃圾回收释放这些内存的。就是可能垃圾回收的频率比较高。
    如果频繁 OOM 的话,两种情况,一种是正常情况,处理请求的过程中,需要大量的内存,但是所有可回收的都已经回收了,还是不够。另一种就是代码写的有问题,该回收的没回收。
    堆内存配置看着比较合理,正常来说留一个 G ,一部分操作系统需要,一部分 JVM 的一些其他机制也需要堆外内存。比如线程堆栈。
    yuemingming
        15
    yuemingming  
       41 天前
    @sagaxu 看着楼主的意思就是 JVM 内部的 OOM 吧。老哥说的这种情况是堆外内存留少了,然后系统把 JVM 进程给干了。
    superhot
        16
    superhot  
    OP
       41 天前
    @yuemingming 在 Tomcat 的 Catalina 日志里看到的内容,正在连续处理请求的时候,每条日志的时间间隔小于 1s ,但明显该次请求还没处理完毕,上一条还是普通异常处理部分的日志,接下来就变成了类似 Tomcat startup 之类的信息,跟上一条隔了大概 30s ~ 70s 左右,以此来判断是重启过了。不过你说的这点确实需要再确认一下,这个重启到底跟 OOM 有没有关系。
    sagaxu
        17
    sagaxu  
       41 天前
    @yuemingming

    /proc/meminfo:
    MemTotal: 3964656 kB
    MemFree: 114400 kB
    MemAvailable: 3092 kB

    他这很明显就是被 OOM killer 杀掉了。

    Java 8 之后若没设置 MaxDirectMemorySize ,默认跟 Xmx 是 1:1 的,OP 的机器 Xmx 超过 2g 有较大几率 OOM 。
    superhot
        18
    superhot  
    OP
       41 天前
    @sagaxu 请问这种情况下,怎样分配内存比较合理呢?我目前的想法也是先减少 Heap 大小试试看,但不知道靠什么来判断 `-Xmx` 的值设为多少比较合适。跟组长提方案的时候得拿证据……头疼。
    zhouhu
        19
    zhouhu  
       41 天前
    @superhot 你们业务量很大?最好 xmx 和 xms 设置为一样的 。一般的业务 1G 就好了。

    Xmx2847m 为什么是 2847 啊,虽然 G1 会自动对齐。
    sagaxu
        20
    sagaxu  
       41 天前   ❤️ 1
    @superhot 需要结合 GC 日志来分析,降低 Xmx 会提高 GC 频率,降低系统吞吐并增加系统延迟。调低 Xmx 后如果性能不达标,一般尝试调整参数优化 GC ,升级 JVM 版本可能也有些帮助。Tomcat 本身也有一些参数可以调整。如果这些都解决不了问题,那只能扩容或者优化代码了。
    zhouhu
        21
    zhouhu  
       41 天前
    我猜你虽然设置了最大 Xmx2847m ,但是 JVM 启动的时候并没有申请那么多,当 运行一段时间后 JVM 再去申请内存,操作系统已经没有足够的内存了。

    所以设置 Xmx2048m Xms2048m

    通过查看不同 region 数量显示,如果是 2048M 还是有空余空间的。

    Xmx2847m

    total 2847

    old: 1733
    free: 937
    HS: 18
    HC:19
    E|cs: 122
    s|cs: 18
    zhouhu
        22
    zhouhu  
       41 天前   ❤️ 2
    我给出的方案是先设置。Xmx2048m Xms2048m 观察
    1. 打印详细 GC 日志 -Xlog:gc*=info ,更为详细的是 debug 和 trace
    2. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump 分析内存镜像。
    3. 观察物理机内存使用情况。
    superhot
        23
    superhot  
    OP
       41 天前
    @zhouhu 没有参考标准,不知道如何判断业务规模,仅凭个人感觉不是很大,但现有架构已经撑不住了,虽然跑了 7 80 台 `t3.medium` 实例,还是经常超时死锁重启什么的…… 主要原因应该是代码质量差,之前的技术负责人跑路,祖传屎山摇摇欲坠。至于为什么是 2847……我也找不到人问 hhhh
    感谢提供思路,我明天再查查 Heap Dump ,印象里好像是有这么个东西的。
    chihiro2014
        24
    chihiro2014  
       41 天前
    用 idea 看看性能分析,该优化优化
    zhouhu
        25
    zhouhu  
       41 天前
    设置了参数 Xmx2048m Xms2048m ,我觉得基本没啥问题,因为从 region 的分布来看,还有 937 个是空闲。
    还有是设置了 Xmx2048m Xms2048m 对系统机器上的其他程序会不会有影响。
    superhot
        26
    superhot  
    OP
       41 天前
    @chihiro2014 实不相瞒,用的 VS Code + Java 插件,真是要多难用有多难用……主要是为了用 Copilot 跟 Devcontainer 。
    facelezz
        27
    facelezz  
       41 天前
    GC 日志加 MAT 分析下 Heap 应该比较好找吧
    facelezz
        28
    facelezz  
       41 天前
    而且你生产 xmx 和 xms 最好设置成一样的
    facelezz
        29
    facelezz  
       41 天前
    这个第一眼看上去是物理内存不够 你虽然标定了 xmx2847m 但实际上没有这么多
    susuper
        30
    susuper  
       41 天前
    堆内存调低,加一下-XX:NativeMemoryTracking=summary 里面的数据汇总,就是你程序总共占用的(堆+非堆)。 目前日志看起来是程序占用+系统占用的 超过设备内存大小,无法分配。 可以适当调低堆内存,GC 频繁点,压缩一下内存占用
    susuper
        31
    susuper  
       41 天前
    目前看程序跑起来堆用了 1908 M ,应该是可以压缩的,2048 稳定的话,就试试 1920,1792 等等,只要堆内存不溢出就行
    zhouhu
        32
    zhouhu  
       41 天前   ❤️ 1
    Heap:
    garbage-first heap total 2916352K, used 1955145K [0x000000074e000000, 0x0000000800000000)
    region size 1024K, 141 young (144384K), 18 survivors (18432K)
    Metaspace used 80907K, capacity 82397K, committed 83740K, reserved 1124352K
    class space used 7289K, capacity 7805K, committed 8316K, reserved 1048576K

    从日志来看使用了 1,909M ,一共是 2,848M ,说明此时 JVM 进程已经占用了 1,909M 。剩下的 939 M 是需要向操作系统申请的。申请的时候出现了机器物理内存不足

    然后从我统计的 Free region 来看恰好是 937 (与 939 有些许误差)。
    此时 cs eden 区: 122 ,survivor cs: 18 ,JVM 完全可以进行垃圾收集。

    可以认为是 JVM 给申请新的 eden region 出现了 OOM 。可能是:
    1. JVM 想申请新的 region ,尝试 GC ,新的 region 作为 evacuate region 。
    2. JVM 认为还有空间剩余,不需要 GC ,直接申请新的 region 。

    PS:HS: 18 可以看到大对象有 18 个,并且 HC:19 个,说明大对象可能大对象超过 18 个(大多数可能大于 1M ),可以将 region 的大小设置为 4 M , -XX:G1HeapRegionSize=4M 。

    PS: 本人写了一些 G1 文章,欢迎 star https://yoa1226.github.io/
    zhouhu
        33
    zhouhu  
       41 天前   ❤️ 1
    GC Heap History (20 events):
    Event: 497209.338 GC heap before
    {Heap before GC invocations=64918 (full 2):
    garbage-first heap total cK, used 2054473K [0x000000074e000000, 0x0000000800000000)
    region size 1024K, 142 young (145408K), 23 survivors (23552K)
    Metaspace used 80903K, capacity 82391K, committed 83740K, reserved 1124352K
    class space used 7288K, capacity 7804K, committed 8316K, reserved 1048576K
    }

    Event: 497209.794 GC heap before
    {Heap before GC invocations=64919 (full 2):
    garbage-first heap total 2916352K, used 2089289K [0x000000074e000000, 0x0000000800000000)
    region size 1024K, 142 young (145408K), 18 survivors (18432K)
    Metaspace used 80903K, capacity 82391K, committed 83740K, reserved 1124352K
    class space used 7288K, capacity 7804K, committed 8316K, reserved 1048576K
    }

    Event: 497210.355 GC heap before
    {Heap before GC invocations=64920 (full 2):
    garbage-first heap total 2916352K, used 2112841K [0x000000074e000000, 0x0000000800000000)
    region size 1024K, 142 young (145408K), 18 survivors (18432K)
    Metaspace used 80903K, capacity 82391K, committed 83740K, reserved 1124352K
    class space used 7288K, capacity 7804K, committed 8316K, reserved 1048576K
    }

    从这些日志可以看出 young region 应该是 142 个,超过 142 就触发 GC 。
    年轻大小如果没有设置是依据 GC 停顿时间(-XX:MaxGCPauseMillis=200 )自动调整的。142 / 2848 = 4.985 % 约等于 5 %

    -XX:G1NewSizePercent=5

    -XX:G1MaxNewSizePercent=60

    发生 OOM 的时间 young region 恰好是 141 个,说明是 《 JVM 认为还有空间剩余,不需要 GC ,直接申请新的 region 》,但是此时物理机器没有新的连续 1M 的内存就发生 OOM 了。


    还有虽然参数设置的是-Xmx2847m ,但实际上 JVM 最大能够申请的是 2848 M 。
    zhouhu
        34
    zhouhu  
       41 天前   ❤️ 1
    从统计 E|cs: 122 + s|cs: 18 = 140 来说,当内存不足时,JVM 完全时可以进行 GC 的,但是你设置的 -Xmx2847m 欺骗了 JVM 。

    之前为什么顺利进行了 GC ?因为之前 young 达到 142 个时,恰好还有物理内存,可以申请 survivor region 作为 evaluate region 。
    chihiro2014
        35
    chihiro2014  
       41 天前
    @superhot emmm ,idea 也有 copilot 。。。没苦硬吃啊
    wangyg
        36
    wangyg  
       41 天前   ❤️ 1
    1.内存使用率 93%是否正常?
    在生产环境中,93%的内存使用率确实偏高。理想情况下,应该保持一定的空闲内存以应对突发的负载增加。通常建议将内存使用率控制在 70-80%左右。高内存使用率可能导致系统性能下降,并增加 OOM 风险。

    2.JVM Heap 大小建议
    根据提供的信息,你的 EC2 实例有 4GB 内存,当前 JVM 堆大小设置为 2847MB 。考虑到操作系统和其他服务也需要内存,这个设置已经很激进了。建议稍微减小堆大小,例如设置为 2560MB (-Xmx2560m )。这样可以为操作系统和其他进程留出更多空间。


    3.JVM 非堆内存使用:
    JVM 非堆内存( Metaspace 、CodeCache 、DirectByteBuffers 等)的使用量因应用程序而异。一般来说,这些区域可能占用 200MB-1GB 左右的内存。
    判断依据:
    - 使用 jconsole 或 jstat 等工具监控实际使用情况
    - 分析堆转储( heap dump )文件
    - 查看 hs_err_pid 日志中的内存使用信息

    4.分析 OOM 的可能情况:
    从你提供的 hs_err_pid 日志中,可以看到以下关键信息:
    ```
    # There is insufficient memory for the Java Runtime Environment to continue.
    # Native memory allocation (mmap) failed to map 16384 bytes for committing reserved memory.
    ```

    这表明 JVM 无法为本机内存分配请求分配更多内存。这可能是由于以下原因造成的:

    a. 物理内存耗尽
    b. 操作系统限制(如 ulimit 设置)
    c. 内存碎片化严重

    分析建议:
    - 使用 jconsole 或 VisualVM 等工具监控 JVM 内存使用情况
    - 分析堆转储文件,查找内存泄漏
    - 检查 GC 日志,了解垃圾回收情况
    - 检查系统日志,查看是否有其他进程占用大量内存
    - 考虑使用 JVM 参数如-XX:NativeMemoryTracking=summary 来跟踪本机内存使用

    总结,有以下几条建议:
    1.调整 JVM 参数:
    ```
    -Xmx2560m -XX:MaxMetaspaceSize=256m -XX:ReservedCodeCacheSize=240m
    ```

    2.启用详细 GC 日志:
    ```
    -Xlog:gc*=info:file=/path/to/gc.log:time,uptime,level,tags:filecount=5,filesize=100m
    ```

    3.使用 jstat 等工具定期监控 JVM 内存使用情况。
    4.检查应用程序代码,寻找可能的内存泄漏。
    5.优化数据库查询和缓存策略,减少内存压力。
    6.考虑使用 JVM 参数如-XX:+HeapDumpOnOutOfMemoryError 来在 OOM 时自动生成堆转储。
    MonkeyJon
        37
    MonkeyJon  
       41 天前
    装个 arthas 排查下
    cowcomic
        38
    cowcomic  
       41 天前
    日志给的听明确的,服务器没可用内存了
    The system is out of physical RAM or swap space


    /proc/meminfo:
    MemTotal: 3964656 kB
    MemFree: 114400 kB
    MemAvailable: 3092 kB

    可用内存才 3M

    你看系统日志/var/log/messages ,应该有 JAVA 进程申请内存失败的日志
    silencegg
        39
    silencegg  
       41 天前
    if (!recoverable_mmap_error(err)) {
    warn_fail_commit_memory(addr, size, exec, err);
    vm_exit_out_of_memory(size, OOM_MMAP_ERROR, "committing reserved memory."); //第 3213 , 结合 NMAP ,估计是堆外内存
    }

    堆外内存不够了
    ZZ74
        40
    ZZ74  
       41 天前
    Metaspace:

    Usage:
    Non-class: 72.84 MB capacity, 71.89 MB ( 99%) used, 786.57 KB ( 1%) free+waste, 187.12 KB ( <1%) overhead.
    Class: 7.62 MB capacity, 7.12 MB ( 93%) used, 446.66 KB ( 6%) free+waste, 68.88 KB ( <1%) overhead.
    Both: 80.47 MB capacity, 79.01 MB ( 98%) used, 1.20 MB ( 1%) free+waste, 256.00 KB ( <1%) overhead.

    Virtual space:
    Non-class space: 74.00 MB reserved, 73.66 MB (>99%) committed


    Metaspace 满了。metaspace 请求内存使用 mmp 的的。
    建议二选一
    物理内存+1G 或者 Heap 减少一些。从你的 GC 情况看 heap 减个 500MB 左右 应该能跑。
    superhot
        41
    superhot  
    OP
       41 天前
    @ZZ74
    @silencegg

    请问是通过什么判断的堆外内存不够的? >99% committed ?`mmap`?

    @zhouhu

    非常感谢,解释得很详细了,但自己太菜了术语太多有点看不懂,想确认一下:

    > 此时 cs eden 区: 122 ,survivor cs: 18 ,JVM 完全可以进行垃圾收集。
    > 从统计 E|cs: 122 + s|cs: 18 = 140 来说,当内存不足时,JVM 完全时可以进行 GC 的

    也就是说,还存在 Young Generation 的话,就意味着还有可以 GC 的空间,而不需要申请新的内存,但因为还没达到 `-Xms` 设置的上限,所以为了性能 JVM 没选择 GC ,而是直接继续申请分配内存了,是这样理解吗?

    另外注意到

    ```
    MaxMetaspaceSize: unlimited
    CompressedClassSpaceSize: 1.00 GB
    Initial GC threshold: 20.80 MB
    Current GC threshold: 136.30 MB
    ```

    单 `CompressedClassSpaceSize` 就有 1G ,加上 `-Xmx2847m`,剩下的内存肯定不够给其他进程分配的,但因为未设置 `-Xms`,所以最开始 JVM 给 Heap 申请的大小没那么多,但随着程序运行,JVM 想要申请更多内存时,算上已经被用掉的堆外内存及其他进程占用的内存后,剩余可用内存不够分配,所以导致 JVM 发生 OOM (但其实),是这样吗?

    把 JVM 参数设为

    ```
    -Xms2560m -Xmx2560m -XX:MaxMetaspaceSize=256m -XX:ReservedCodeCacheSize=240m
    ```

    是否可行?
    zhouhu
        42
    zhouhu  
       41 天前
    前面老哥们都说得很好。

    物理内存不变的情况下,把 java 内存减少到 1.5G-2G 范围内应该是可行的。元空间的话也设置最大最小一样的吧

    当年轻代耗尽时候会进行 Young GC 。

    CompressedClassSpaceSize: 1.00 GB 这个参数是当前默认设置吧?
    superhot
        43
    superhot  
    OP
       40 天前
    感谢楼上各位老哥,先临时把 Heap 调低了一点,暂时没出什么问题,跟组长汇报了一下,之后考虑加上 Heap Dump 再观察一段时间。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5418 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 07:48 · PVG 15:48 · LAX 23:48 · JFK 02:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.