这是《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
如果同时设置 -Xmx 和 MaxRAMPercentage,最终结果要按 JVM 规则确认,不要靠猜。
28.4 CPU 感知
JVM 会根据感知到的 CPU 数决定 GC 线程、JIT 编译线程等默认并发度。
常见问题:
- 宿主机 64 核,Pod CPU limit 2 核;
- JVM 启动过多 GC 线程;
- CPU throttling 导致延迟毛刺;
- 编译线程与业务线程争抢;
- 并行流或框架线程池使用错误 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。
建议:
- Java 进程作为 PID 1 或正确转发信号;
- Spring Boot 等框架开启优雅停机;
- 停止接收新请求;
- 等待存量请求完成;
- 关闭线程池和连接池;
- 主动释放应用锁;
- 超时时间小于 terminationGracePeriodSeconds。
常见错误是在 shell 脚本中启动 Java 后没有 exec:
java -jar app.jar
更稳妥:
exec java -jar /app/app.jar
否则 SIGTERM 可能发给 shell,Java 进程收不到。
28.8 镜像与运行时
选择基础镜像时关注:
- JDK 还是 JRE;
- 是否包含诊断工具;
- glibc 还是 musl;
- 字体、时区、语言包;
- 镜像安全修复策略;
- 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。
思考题
- 为什么
-Xmx等于容器 memory limit 是危险的? - JVM 的 GC 线程数为什么会受容器 CPU limit 影响?
- Kubernetes exit code 137 说明什么?
- 如何保证 Pod 重建后诊断文件仍可使用?
- 启动脚本为什么常用
exec java?