JVMNotes

第 28 章:容器中的 JVM

zjc 于 2026-01-28 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 容器中的 JVM 问题,常常不是 JVM 单独的问题,而是 JVM、操作系统 cgroup、Kubernetes 调度和应用资源模型共同作用的结果。本章重点讲 JVM 在容器中如何感知资源,以及如何配置内存、CPU、日志和诊断。

28.1 与物理机部署的差异

维度 物理机/虚拟机 容器
CPU 看到整机核数 受 CPU limit 约束
内存 看到系统内存 受 cgroup memory limit 约束
线程限制 系统 ulimit 还可能受 pids cgroup 限制
OOM 进程或系统行为 可能被 OOMKilled
存储 本地盘 依赖挂载卷
诊断 直接读文件 需考虑 Pod 生命周期

JVM 看到的资源与实际可用资源不一致,是最常见的问题来源。

28.2 内存模型

Java 进程内存不只是堆:

容器 memory limit
  Java Heap
  Metaspace
  Thread stacks
  Direct memory
  CodeCache
  GC 开销
  Symbol/Internal
  Native library
  OS 余量

如果只设置:

容器 limit = 4Gi
-Xmx4g

堆外内存稍一增加,就可能 OOMKilled。

推荐使用比例并预留空间:

java -XX:MaxRAMPercentage=60.0 -jar app.jar

根据应用特点,常见区间在 50% 到 70%。堆外占用高的 Netty、RPC、缓存类应用应从更低比例开始。

28.3 容器内存参数

常用参数:

参数 含义
-XX:+UseContainerSupport 启用容器感知
-XX:InitialRAMPercentage 初始堆占容器内存比例
-XX:MaxRAMPercentage 最大堆占容器内存比例
-XX:MinRAMPercentage 小内存容器场景
-XX:ActiveProcessorCount 显式指定处理器数

示例:

java -XX:+UseContainerSupport \
  -XX:InitialRAMPercentage=60.0 \
  -XX:MaxRAMPercentage=60.0 \
  -XX:ActiveProcessorCount=4 \
  -jar app.jar

如果同时设置 -XmxMaxRAMPercentage,最终结果要按 JVM 规则确认,不要靠猜。

28.4 CPU 感知

JVM 会根据感知到的 CPU 数决定 GC 线程、JIT 编译线程等默认并发度。

常见问题:

  1. 宿主机 64 核,Pod CPU limit 2 核;
  2. JVM 启动过多 GC 线程;
  3. CPU throttling 导致延迟毛刺;
  4. 编译线程与业务线程争抢;
  5. 并行流或框架线程池使用错误 CPU 数。

显式指定:

-XX:ActiveProcessorCount=4

对于 cpu limit = 2 的 Pod,将其设置为远大于 2 通常是不合适的。

28.5 request 与 limit

Kubernetes 中:

字段 作用
request 调度依据和共享资源保障
limit cgroup 上限

常见配置:

resources:
  requests:
    cpu: "2"
    memory: "4Gi"
  limits:
    cpu: "4"
    memory: "4Gi"

内存 request 与 limit 通常需要一致,避免调度时认为资源够、运行时却触发 OOM。CPU 可以允许 burst,但要评估 throttling。

28.6 诊断文件布局

推荐挂载:

/logs       GC 日志、JFR
/data/dump  堆 dump
/tmp        临时文件,可单独限制

示例启动参数:

java -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \
  -XX:StartFlightRecording=filename=/logs/app.jfr,maxsize=512m,maxage=6h,dumponexit=true \
  -jar /app/app.jar

如果日志写镜像可写层,Pod 重建后会丢失。使用 emptyDir 也会随 Pod 删除,重要诊断文件需要外发或挂持久卷。

28.7 优雅停机

Kubernetes 会先向容器进程发送 SIGTERM,等待 terminationGracePeriodSeconds 后再 SIGKILL。

建议:

  1. Java 进程作为 PID 1 或正确转发信号;
  2. Spring Boot 等框架开启优雅停机;
  3. 停止接收新请求;
  4. 等待存量请求完成;
  5. 关闭线程池和连接池;
  6. 主动释放应用锁;
  7. 超时时间小于 terminationGracePeriodSeconds。

常见错误是在 shell 脚本中启动 Java 后没有 exec

java -jar app.jar

更稳妥:

exec java -jar /app/app.jar

否则 SIGTERM 可能发给 shell,Java 进程收不到。

28.8 镜像与运行时

选择基础镜像时关注:

  1. JDK 还是 JRE;
  2. 是否包含诊断工具;
  3. glibc 还是 musl;
  4. 字体、时区、语言包;
  5. 镜像安全修复策略;
  6. JVM 是否匹配目标架构。

Alpine 类镜像小,但 musl libc 对某些 Native 库、内存分配行为和诊断工具会有差异。追求稳定时,使用发行版标准运行时通常更省心。

28.9 监控指标

至少监控:

指标 目的
Pod memory usage 是否接近 limit
Pod CPU usage/throttling CPU 是否受限
JVM heap used/committed/max 堆趋势
GC pause 延迟影响
Metaspace 类元数据
Direct memory 堆外
Thread count 线程增长
Restart count 稳定性
OOMKilled event 容器级问题

只监控堆会漏掉 OOMKilled。必须同时看进程 RSS 和容器 limit。

28.10 常见故障

现象 可能原因 排查
exit 137 OOMKilled memory limit、堆外
延迟毛刺 CPU throttling、GC 监控和 GC 日志
启动慢 CPU 低、JIT 就绪探针、CPU
信号不生效 Java 不是 PID 1 检查 ENTRYPOINT
dump 丢失 本地文件随 Pod 删除 挂卷或上传
GC 线程过多 CPU 感知错误 ActiveProcessorCount
频繁重启 健康检查过严或 OOM 事件和应用日志

本章小结

容器中 JVM 的关键是资源感知和总内存预算。堆、元空间、线程栈、直接内存、CodeCache 和 Native 都必须放入容器 limit;CPU limit 会影响 GC、JIT 和业务线程调度。诊断文件要挂载并考虑 Pod 生命周期,优雅停机要确保 Java 进程能收到 SIGTERM。

思考题

  1. 为什么 -Xmx 等于容器 memory limit 是危险的?
  2. JVM 的 GC 线程数为什么会受容器 CPU limit 影响?
  3. Kubernetes exit code 137 说明什么?
  4. 如何保证 Pod 重建后诊断文件仍可使用?
  5. 启动脚本为什么常用 exec java