K8S 上 java.lang.OutOfMemoryError 的相关问题与解决方法
目录
1. java.lang.OutOfMemoryError: Java heap space
2. java.lang.OutOfMemoryError: Metaspace
3. java.lang.OutOfMemoryError: GC Overhead limit exceeded
4. java.lang.OutOfMemoryError: Direct buffer memory
5. java.lang.OutOfMemoryError: Unable to create new native thread
6. java.lang.OutOfMemoryError: Requested array size exceeds VM limit
在 Kubernetes(K8s)中,java.lang.OutOfMemoryError(java OOM)问题可能由多种原因导致,包括 Java 堆内存不足、元空间(Metaspace)耗尽、直接内存(Direct Memory)溢出等。以下列出针对不同 java 层面 OOM 错误的排查和解决方法。有需要的小伙伴自查。
目前我在生产环境中应用过,能有效控制的java相关内存的参数也放在最后。
1. java.lang.OutOfMemoryError: Java heap space
原因:
① Java 堆内存不足,无法分配新对象。
② 程序中存在大量未被回收的对象(内存泄漏),或者需要分配的对象超出了堆内存的大小限制。
(1)调整 Pod 内存限制
* 在 Deployment 或 StatefulSet 的 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 MAT 或 VisualVM 分析内存泄漏:
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
更多推荐



所有评论(0)