目录

1. java.lang.OutOfMemoryError: Java heap space

(1)调整 Pod 内存限制

(2)优化 JVM 参数

(3)分析堆内存泄漏

2. java.lang.OutOfMemoryError: Metaspace

(1)增加 Metaspace 大小

(2)检查动态类生成

(3)减少重复类加载

3. java.lang.OutOfMemoryError: GC Overhead limit exceeded

(1)调整 GC 策略

(2)优化堆内存分配

(3)检查 Pod 内存限制

4. java.lang.OutOfMemoryError: Direct buffer memory

(1)增加 JVM 直接内存限制

(2)优化 Netty、Kafka 等框架配置

(3)检查 kubectl top pod

5. java.lang.OutOfMemoryError: Unable to create new native thread

(1)调整 Pod 的 securityContext

(2)优化线程池配置

(3)检查宿主机 ulimit

6. java.lang.OutOfMemoryError: Requested array size exceeds VM limit

(1)分块处理数据

(2)优化算法

7. 实际K8S生产环境应用


在 Kubernetes(K8s)中,java.lang.OutOfMemoryError(java OOM)问题可能由多种原因导致,包括 Java 堆内存不足、元空间(Metaspace)耗尽、直接内存(Direct Memory)溢出等。以下列出针对不同 java 层面 OOM 错误的排查和解决方法。有需要的小伙伴自查。

目前我在生产环境中应用过,能有效控制的java相关内存的参数也放在最后。


1. java.lang.OutOfMemoryError: Java heap space

原因

      ① Java 堆内存不足,无法分配新对象。

      ② 程序中存在大量未被回收的对象(内存泄漏),或者需要分配的对象超出了堆内存的大小限制。

(1)调整 Pod 内存限制

        * 在 DeploymentStatefulSet 的 yaml 中增加 resources.limits.memory,如:

resources:
  limits:
    memory: "2Gi"
  requests:
    memory: "1Gi"

        * 如果 JVM 未正确识别容器内存限制,需设置 -XX:MaxRAMPercentage(JDK 8u191+ 或 JDK 10+),如:

env:
  - name: JAVA_OPTS
    value: "-XX:MaxRAMPercentage=70.0"  # 使用 70% 的 Pod 内存限制

(2)优化 JVM 参数

        * 手动设置 -Xms 和 -Xmx ,如:

env:
  - name: JAVA_OPTS
    value: "-Xms512m -Xmx1024m"

注意:

-Xms 和 -Xmx 必须小于 Pod 的 limits.memory,设置的值一般为容器内存限制resources.limits.memory 的70% - 80% 。

(3)分析堆内存泄漏

        * 启用 Heap Dump:

env:
  - name: JAVA_OPTS
    value: "-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof"

        * 使用 kubectl cp 导出 Heap Dump 文件,然后可以使用 Eclipse MATVisualVM 分析内存泄漏:

kubectl cp <pod-name>:/tmp/heapdump.hprof ./heapdump.hprof

2. java.lang.OutOfMemoryError: Metaspace

原因:加载的类元数据超出 Metaspace 限制(Metaspace 是 JVM 8 及以上版本中取代永久代的内存区域,用于存储类元数据。如果加载过多的类,可能导致 Metaspace 内存耗尽)。

(1)增加 Metaspace 大小

        * 在 JVM 参数中设置 -XX:MaxMetaspaceSize,如:

env:
  - name: JAVA_OPTS
    value: "-XX:MaxMetaspaceSize=256m"

(2)检查动态类生成

        * 避免滥用反射、动态代理(如 CGLIB)、字节码增强(如 ASM)。

(3)减少重复类加载

        * 使用共享类加载机制(如 Spring Boot DevTools 的 restartClassLoader)。


3. java.lang.OutOfMemoryError: GC Overhead limit exceeded

原因

      ① JVM 花费过多时间在垃圾回收(GC)上,但回收效果极差。

      ② 内存压力过大,导致 GC 频繁运行,但效果不佳

(1)调整 GC 策略

        * 使用 G1 GC(推荐):

env:
  - name: JAVA_OPTS
    value: "-XX:+UseG1GC"

        * 或可以添加 G1GC 关键参数:

#在“-XX:+UseG1GC”可添加如下参数(注意在两个参数间需要加一个空格):

#这个参数告诉垃圾回收器,每次垃圾回收操作的暂停时间不应超过指定的毫秒数。
-XX:MaxGCPauseMillis=200
#拓展:垃圾回收器会尝试在每次垃圾回收时,将暂停时间控制在目标值以内。如果实际暂停时间超过这个值,垃圾回收器可能会调整其行为,例如增加垃圾回收的频率或调整堆的大小。

#设置触发垃圾回收(GC)的堆占用率阈值,当堆的占用率达到所设置的百分比时,触发垃圾回收。对于 G1 垃圾回收器,建议的默认值是45。
-XX:InitiatingHeapOccupancyPercent=45

#设置 G1 垃圾回收器的保留区域大小,单位为百分比。
-XX:G1ReservePercent=15
#拓展:G1 垃圾回收器会保留一部分堆内存,用于处理突发的内存分配需求,可以减少垃圾回收的频率和时间。

#设置并行垃圾回收线程的数量。
-XX:ParallelGCThreads=4
#拓展:默认值通常取决于你的系统 CPU 核心数。JVM 会根据系统的核心数自动选择一个合适的值,如果你的系统有8个核心,JVM 可能会默认设置为4或8。

(2)优化堆内存分配

        * 避免频繁创建大对象(如缓存未清理的 HashMap)。

(3)检查 Pod 内存限制

        * 确保 resources.limits.memory 足够,避免频繁触发 OOM Killer。


4. java.lang.OutOfMemoryError: Direct buffer memory

原因:NIO(Java New Input/Output)使用的堆外内存(Direct Buffer)耗尽。Direct Memory 是通过 ByteBuffer.allocateDirect() 分配的,大小受 -XX:MaxDirectMemorySize 控制。

(1)增加 JVM 直接内存限制

        * 设置 -XX:MaxDirectMemorySize

env:
  - name: JAVA_OPTS
    value: "-XX:MaxDirectMemorySize=512m"

(2)优化 Netty、Kafka 等框架配置

        * 例如,调整 Netty 的 ByteBuf 池大小(java代码添加):

-Dio.netty.maxDirectMemory=0  # 禁用 Netty 的堆外内存限制

(3)检查 kubectl top pod

        * 确认 Pod 内存使用是否接近 limits.memory


5. java.lang.OutOfMemoryError: Unable to create new native thread

原因:线程数超过系统限制(ulimit -u)。

(1)调整 Pod 的 securityContext

        * 增加 fs.inotify.max_user_instances

securityContext:
  sysctls:
    - name: fs.inotify.max_user_instances
      value: "8192"

(2)优化线程池配置

        * 使用 Executors.newFixedThreadPool 限制最大线程数。

(3)检查宿主机 ulimit

        * 如果 Pod 运行在特权模式,可能需要调整节点 OS 限制:

ulimit -u 65535

6. java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:分配的数组大小超过了 JVM 的限制,通常是 2GB(如 Integer.MAX_VALUE - 2)。

(1)分块处理数据

        * 避免单次加载超大数组,改用流式处理(如 Stream)。

(2)优化算法

        * 使用更高效的数据结构(如 ByteBuffer 替代 byte[])。


7. 实际K8S生产环境应用

我在生产环境的特定工作负载(P.S. 当前内存预留8G,限制10G;此前内存预留3G,限制8G)的环境变量 JAVA_OPTS 配置如下(仅供参考):

-XX:+UseContainerSupport              # 启用容器内存感知
-XX:MaxRAMPercentage=70.0        # 堆占容器内存的70%(8G*70%=5.6G)
-XX:InitialRAMPercentage=70.0
-XX:+UseG1GC
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=384M           # 降低元空间上限
-XX:MaxDirectMemorySize=512M      # 限制堆外内存
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=65   # 提高触发阈值
-XX:G1ReservePercent=10            # 减少预留空间
-XX:+PerfDisableSharedMem        # 禁用perfdata,防止SIGBUS
-XX:+AlwaysPreTouch                 # 启动时预分配内存
-XX:NativeMemoryTracking=summary     # 监控Native内存

对以上参数解析如下:

(1)-XX:+UseContainerSupport

           启用 JVM 对容器环境的感知能力。让 JVM 自动检测容器(如 Docker/K8s)设置的内存限制,而不是使用宿主机的内存总量。

(2)-XX:MaxRAMPercentage=70.0

           设置堆内存最大可占用容器总内存的百分比。相比固定值 -Xmx 更灵活,容器扩容时自动适应。设置后,容器内存 8GB × 70% = 5.6GB(堆最大内存),原来堆最大内存是是设置固定值“-Xmx5120m”。

(3)-XX:InitialRAMPercentage=70.0

           设置堆内存初始占用容器内存的百分比。通常与 MaxRAMPercentage 设相同值,避免堆动态调整的开销。

(4)-XX:+UseG1GC

           启用 G1 垃圾收集器。面向大堆(>4GB)、低延迟(<200ms)场景设计。可预测的停顿时间模型,分区回收减少 Full GC。

(5)-XX:MetaspaceSize=256M

           设置元空间初始大小,存储类元数据(非堆内存)。注意设置的这个不是上限值,JVM 会根据需要自动扩展。

(6)-XX:MaxMetaspaceSize=384M

           设置元空间内存上限,防止类加载器泄漏导致无限占用内存。

(7)-XX:MaxDirectMemorySize=512M

           限制直接内存(堆外内存)大小,使用场景为NIO 操作、Netty 等框架,可防止堆外内存失控导致容器 OOM。

(8)-XX:MaxGCPauseMillis=200

           设置 G1 GC 的目标最大停顿时间,单位为毫秒,值越小 GC 越频繁,吞吐量越低。

(9)-XX:InitiatingHeapOccupancyPercent=65

           设置触发并发标记周期的堆占用阈值。原本我设置的是45%,现在改为65%,可延迟 GC 触发时机,5.6GB × 65% = 3.64GB 时开始标记(原配置 5GB × 45% = 2.25GB 就触发)。

(10)-XX:G1ReservePercent=10

           设置 G1 保留内存占堆的百分比,这是应急内存,可避免晋升失败导致的 Full GC)。

(11)-XX:+PerfDisableSharedMem

           禁用 JVM 性能数据共享内存,可彻底避免 SIGBUS 错误,但是牺牲少量监控能力,生产环境可以接受的。

此工作负载在设置该java环境变量前,曾出现过如下报错:


# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGBUS (0x7) at pc=0x00007ffff684af96, pid=6, tid=0x00007ffe581fa700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_212-b10) (build 1.8.0_212-b10)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.212-b10 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# V  [libjvm.so+0x43bf96]  PerfClassTraceTime::initialize()+0x26
#
# Core dump written. Default location: //core or core.6
#
# An error report file with more information is saved as:
# //hs_err_pid6.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp  
#
Aborted (core dumped)

(12)-XX:+AlwaysPreTouch

           启动时预分配并初始化所有内存页。可以避免运行时缺页中断,让内存分配更稳定,提升运行时性能。缺点就是增加 JVM 启动时间。

(13)-XX:NativeMemoryTracking=summary

           启用原生内存跟踪,ummary(基本摘要信息)。可通过如下命令观察堆外内存分布(线程、代码缓存、GC等)。

jcmd <pid> VM.native_memory summary

--- 以上参数关系如图所示。


补充

对7.(13)进行补充。

在执行命令 jcmd <pid> VM.native_memory summary 时若报错提示如下:

root@las-service-7c59f87586-g5flj:/# jcmd 6 VM.native_memory summary
6:
java.io.IOException: well-known file /tmp/.java_pid6 is not secure: file should be owned by the current user (which is 0) but is owned by -2
        at sun.tools.attach.LinuxVirtualMachine.checkPermissions(Native Method)
        at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:117)
        at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
        at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
        at sun.tools.jcmd.JCmd.executeCommandForPid(JCmd.java:147)
        at sun.tools.jcmd.JCmd.main(JCmd.java:131)

这个错误表明 jcmd 工具在尝试连接到进程 ID 为 6 的 Java 进程时,发现 /tmp/.java_pid6 文件的所有者不是当前用户(UID 为 0,即 root 用户),而是另一个用户(UID 为 -2)。这通常是因为 jcmd 工具需要对目标进程的 /tmp/.java_pid 文件具有访问权限,而该文件的所有者与当前用户不匹配。

解决方法:

1、方法一

① 检查文件所有权(显示文件的所有者和权限信息)。

ls -l /tmp/.java_pid6

② 如果文件的所有者不是当前用户,可以使用 chown 命令更改文件的所有权。

sudo chown 0:0 /tmp/.java_pid6

    这就把文件的所有者和组更改为 root 用户(UID 为 0)。

③ 重新执行命令 jcmd 进行尝试。

2、方法二

如果文件的所有者无法更改,或者文件已经损坏,可以尝试删除该文件,然后重新运行 jcmd 命令。Java 进程会在需要时重新创建该文件。

sudo rm /tmp/.java_pid6

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐