写点什么

【JVM 故障问题排查心得】「内存诊断系列」Xmx 和 Xms 的大小是小于 Docker 容器以及 Pod 的大小的,为啥还是会出现 OOMKilled?

作者:洛神灬殇
  • 2023-01-01
    江苏
  • 本文字数:3052 字

    阅读完需:约 10 分钟

【JVM故障问题排查心得】「内存诊断系列」Xmx和Xms的大小是小于Docker容器以及Pod的大小的,为啥还是会出现OOMKilled?

为什么我设置的大小关系没有错,还会 OOMKilled?

这种问题常发生在 JDK8u131 或者 JDK9 版本之后所出现在容器中运行 JVM 的问题:在大多数情况下,JVM 将一般默认会采用宿主机 Node 节点的内存为 Native VM 空间(其中包含了堆空间、直接内存空间以及栈空间),而并非是是容器的空间为标准。

堆内存和 VM 实际分配内存不一致

-XshowSettings:vm

Jps -lVvm


我们在运行的时候将 JVM 堆内存内存设置为 3000MB,而-XshowSettings:vm 打印出的 JVM 将最大堆大小为 1.09G,如果按照这个内存进行分配内存的话很可能会导致实际内存和预分配内存所造成的不一致问题。

如何解决此问题

JVM 感知 cgroup 限制

解决 JVM 内存超限的问题,这种方法可以让 JVM 自动感知 Docker 容器的 cgroup 限制,从而动态的调整堆内存大小。


JDK8u131 在 JDK9 中有一个很好的特性,即 JVM 能够检测在 Docker 容器中运行时有多少内存可用。为了使 jvm 保留根据容器规范的内存,必须设置标志-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap


注意:如果将这两个标志与 Xms 和 Xmx 标志一起设置,那么 jvm 的行为将是什么?-Xmx 标志将覆盖-XX:+ UseCGroupMemoryLimitForHeap 标志


参数分析

  • -XX:+ UseCGroupMemoryLimitForHeap标志使 JVM 可以检测容器中的最大堆大小。

  • -Xmx标志将最大堆大小设置为固定大小。


除了 JVM 的堆空间,还会对于非堆 Noheap 和 JVM 的东西,还会有一些额外的内存使用情况。

使用 JDK9 的容器感知机制尝试

设置了容器有 4GB 内存分配,而 JVM 使用 1GM 作为最大堆,因为容器中除了 JVM 之外没有其他进程在运行,所以我们还可以进一步扩大一下对于 Heap 堆的分配?

-XX:MaxRAMFraction

在较低的版本的时候可以使用-XX:MaxRAMFraction 参数,它告诉 JVM 使用可用内存/MaxRAMFract 作为最大堆。使用-XX:MaxRAMFraction=1,我们将几乎所有可用内存用作最大堆。

问题分析

最大堆占用总内存是否仍然会导致你的进程因为内存的其他部分(如“元空间”)而被杀死?


答案:MaxRAMFraction=1 仍将为其他非堆内存留出一些空间



注意:如果容器使用堆外内存,这可能会有风险,因为几乎所有的容器内存都分配给了堆。您必须将-XX:MaxRAMFraction=2 设置为堆只使用 50%的容器内存,或者使用 Xmx。


容器内部感知 CGroup 资源限制

Docker1.7 开始将容器 cgroup 信息挂载到容器中,所以应用可以从 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件获取内存、 CPU 等设置,在容器的应用启动命令中根据 Cgroup 配置正确的资源设置 -Xmx, -XX:ParallelGCThreads 等参数

Java10 中,改进了容器集成

Java10+废除了-XX:MaxRAM 参数,因为 JVM 将正确检测该值。在 Java10 中,改进了容器集成,无需添加额外的标志,JVM 将使用 1/4 的容器内存用于堆。


java10+确实正确地识别了内存的 docker 限制,但您可以使用新的标志 MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是旧的 MaxRAMFraction,以便更精确地调整堆的大小。


java10+上的 UseContainerSupport 选项,而且是默认启用的,不用设置。同时 UseCGroupMemoryLimitForHeap 这个就弃用了,不建议继续使用,同时还可以通过 -XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage 这些参数更加细腻的控制 JVM 使用的内存比率。

-XX:MaxRAMFraction

Java 程序在运行时会调用外部进程、申请 Native Memory 等,所以即使是在容器中运行 Java 程序,也得预留一些内存给系统的。所以 -XX:MaxRAMPercentage 不能配置得太大。当然仍然可以使用-XX:MaxRAMFraction=1 选项来压缩容器中的所有内存。


上面我们知道了如何进行设置和控制对应的堆内存和容器内存的之间的关系,所以防止 JVM 的堆内存超过了容器内存,导致容器出现 OOMKilled 的情况。但是在整个 JVM 进程体系而言,不仅仅只包含了 Heap 堆内存,其实还有其他相关的内存存储空间是需要我们考虑的,一边防止这些内存空间会造成我们的容器内存溢出的场景。

Off Heap Space

接下来了我们需要进行分析出 heap 之外的一部分就是对外内存就是 Off Heap Space,也就是 Direct buffer memory 堆外内存。主要通过的方式就是采用 Unsafe 方式进行申请内存,大多数场景也会通过 Direct ByteBuffer 方式进行获取。好废话不多说进入正题。


JVM 参数 MaxDirectMemorySize

研究一下 jvm 的-XX:MaxDirectMemorySize,该参数指定了 DirectByteBuffer 能分配的空间的限额,如果没有显示指定这个参数启动 jvm,默认值是 xmx 对应的值(低版本是减去幸存区的大小)。



而 Runtime.maxMemory()在 HotSpot VM 里的实现是:


-Xmx 减去一个 survivor space 的预留大小


DirectByteBuffer 对象是一种典型的”冰山对象”,在堆中存在少量的泄露的对象,但其下面连接用堆外内存,这种情况容易造成内存的大量使用而得不到释放



-XX:MaxDirectMemorySize=size 用于设置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的单位可以使用 k/K、m/M、g/G;如果没有设置该参数则默认值为 0,意味着 JVM 自己自动给 NIO direct-buffer allocations 选择最大大小。

-XX:MaxDirectMemorySize 的默认值是什么?

  • 在 sun.misc.VM 中,它是Runtime.getRuntime.maxMemory(),这就是使用-Xmx 配置的内容。而对应的 JVM 参数如何传递给 JVM 底层的呢?主要通过 hotspot/share/prims/jvm.cpp。

  • jvm.cpp 里头有一段代码用于把 -XX:MaxDirectMemorySize 命令参数转换为 key 为 sun.nio.MaxDirectMemorySize 的属性。我们可以看出来他转换为了该属性之后,进行设置和初始化直接内存的配置。针对于直接内存的核心类就在, 在-XX:MaxDirectMemorySize 是用来配置 NIO direct memory 上限用的 VM 参数。但如果不配置它的话,direct memory 默认最多能申请多少内存呢?这个参数默认值是-1,显然不是一个“有效值”。


sun.nio.MaxDirectMemorySize 属性,如果为 null 或者是空或者是 - 1,那么则设置为 Runtime.getRuntime ().maxMemory ();因为当 MaxDirectMemorySize 参数没被显式设置时它的值就是-1,在 Java 类库初始化时 maxDirectMemory()被 java.lang.System 的静态构造器调用。


这个 max_capacity()实际返回的是 -Xmx 减去一个 survivor space 的预留大小

结论分析说明

MaxDirectMemorySize 没显式配置的时候,NIO direct memory 可申请的空间的上限就是-Xmx 减去一个 survivor space 的预留大小。例如如果您不配置-XX:MaxDirectMemorySize 并配置-Xmx5g,则"默认" MaxDirectMemorySize 也将是 5GB-survivor space 区,并且应用程序的总堆+直接内存使用量可能会增长到 5 + 5 = 10 Gb 。

其他获取 maxDirectMemory 的值的 API 方法

BufferPoolMXBean 及 JavaNioAccess.BufferPool (通过 SharedSecrets 获取) 的 getMemoryUsed 可以获取 direct memory 的大小;其中 java9 模块化之后,SharedSecrets 从原来的 sun.misc.SharedSecrets 变更到 java.base 模块下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 将其导出到 UNNAMED,这样才可以运行

内存分析问题

-XX:+DisableExplicitGC 与 NIO 的 direct memory

用了-XX:+DisableExplicitGC 参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何 GC(但是“函数调用”本身的开销还是存在的哦~)。


做 ygc 的时候会将新生代里的不可达的 DirectByteBuffer 对象及其堆外内存回收了,但是无法对 old 里的 DirectByteBuffer 对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题,如果有大量的 DirectByteBuffer 对象移到了 old,但是又一直没有做 cms gc 或者 full gc,而只进行 ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为 heap 明明剩余的内存还很多(前提是我们禁用了 System.gc)。

发布于: 2023-01-01阅读数: 16
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【JVM故障问题排查心得】「内存诊断系列」Xmx和Xms的大小是小于Docker容器以及Pod的大小的,为啥还是会出现OOMKilled?_jdk_洛神灬殇_InfoQ写作社区