Java 25 on Kubernetes:默认配置正在拖垮你的性能
目录
Akamas 发布的 The State of Java on Kubernetes 2026 提出了一个让人不安的论断:大多数跑在 Kubernetes 上的 Java 应用,默认配置正在悄无声息地浪费资源、降低性能,甚至让服务在压力下直接不可用。
联系到最近我碰到的生产环境上的 oom 和 CPU 限制问题。于是我用 Java 25 + Spring Boot 3.5.1 + kind 本地集群搭了一套实验环境,用真实数据来验证这些说法。
结论先说:基本都是真的,修复成本极低,收益巨大。
实验设计#
用一个 Spring Boot 应用暴露两个压测端点:
GET /stress/memory?mb=N:分配 N MB 短生命周期对象,触发 GCGET /stress/cpu?seconds=N:持续计算质数 N 秒,消耗 CPU
k6 混合打压(60% 内存请求 + 40% CPU 请求,10 VUs,60 秒),通过 Spring Actuator 采集 JVM 指标。
基准容器规格:1c / 1Gi,4 个场景:
| 场景 | JVM 参数 | 资源 | 验证目标 |
|---|---|---|---|
| 01-default | 无 | 1c / 1Gi | 默认配置基准 |
| 02-heap-fixed | MaxRAMPercentage=75 |
1c / 1Gi | 修复堆大小 |
| 03-cpu-throttle | MaxRAMPercentage=75 |
250m / 1Gi | CPU 节流影响 |
| 04-pod-small | MaxRAMPercentage=75 |
0.5c / 1Gi × 3 | 小副本集群 |
| 04-pod-large | MaxRAMPercentage=75 |
1.5c / 1Gi × 1 | 大副本集群 |
GC 类型全部由 JVM 自动选择(Java 25 在 1c 以上默认为 G1GC)。
问题一:默认堆只用了 25%#
这是文章最核心的论断,也是最容易验证的。
运行场景 01(无任何 JVM 参数,1Gi 容器)后,Actuator 给出:
jvm.memory.max (heap) = 247.5 MB
而 1Gi ≈ 1,024 MB。JVM 只用了 24.2% 的容器内存。
加上 -XX:MaxRAMPercentage=75 之后(场景 02):
jvm.memory.max (heap) = 742.4 MB
变成了 72.5%,差距整整 3 倍。
为什么默认只有 25%?#
在 2026 年,随着 Ubuntu 24.04+ 和 RHEL 10+ 等主流发行版彻底废弃 Cgroup v1,Java 25 + Cgroup v2 成为了唯一的标准。JVM 不再需要兼容破碎的 v1 接口,而是直接通过 /sys/fs/cgroup 实现原生感知。
但即便感知准确,JVM 默认的三个百分比参数依然非常保守:
MaxRAMPercentage=25.0:在容器内存 > 250MB 时,最大堆内存仅为容器内存的 25%。InitialRAMPercentage=1.5625:初始堆内存小到可以忽略不计(1Gi 容器只有 ~16MB),这会导致启动时频繁触发 GC 和堆扩容。MinRAMPercentage=50.0:这是一个极具误导性的名字——它实际上是用于内存较小(通常 < 250MB)环境下的最大堆内存百分比。
在 Kubernetes 这种「单容器单进程」的场景下,这些默认值会导致 75% 的资源闲置,且启动性能极差。
2026 年的平衡点:MaxRAMPercentage=75.0#
在 Java 8/11 时代,我们通常建议设为 70-75% 以预留非堆内存(元空间、线程栈等)。
虽然在 Java 25 时代,由于 虚拟线程 (Virtual Threads) 的大规模应用,传统平台线程创建的 Xss 开销大幅减少(理论上可以上调至 80%),但考虑到大多数生产环境仍有 Netty 直接内存、元空间波动等不确定性,75% 依然是最稳健、最通用的金科玉律。
进阶建议:如果你的服务是非 I/O 密集型且经过严格压测,确认非堆占用极低,可以尝试将其上调至 80% 以压榨极致性能。
更进一步:实现 Guaranteed QoS (服务质量保证)
为了避免堆动态扩容带来的系统调用开销以及 Kubernetes 中的 CPU 抖动,应将 InitialRAMPercentage 设为与 MaxRAMPercentage 相同的值(即 75.0),实现“启动即满额”。
这 3 倍差距在什么时候会出问题?#
这里有一个微妙之处:如果你的应用只产生短生命周期对象(比如本实验这种纯 GC 压测),G1GC 在 247.5 MB 和 742.4 MB 下的吞吐和延迟可以相差无几——因为年轻代垃圾收集得足够快。
但以下场景会让默认配置直接翻车:
- 内存峰值:请求处理需要临时持有 > 247 MB 数据 → OOM Kill
- 应用缓存:Spring Cache、Caffeine、本地缓存 → 老年代累积 → Full GC 或 OOM
- 连接池 + Session:数据库连接池、HTTP 客户端、会话对象 → 长生命周期对象堆积
- 大对象处理:图片处理、JSON 反序列化大 Payload → 直接分配进老年代
结论:这个问题是真实的。 修复成本是零——加一个 JVM 参数,一行配置。
问题二:CPU 节流比你想象的更危险#
这是本实验最震撼的发现,也是文章所说的「CPU limits function as time quotas enforced by Linux CFS」。
实验结果#
将 CPU limit 从 1000m 降到 250m(场景 03),观察到:
| 指标 | 1000m CPU | 250m CPU | 变化 |
|---|---|---|---|
| GC 最大停顿 | 28 ms | 318 ms | 11 倍 |
| k6 成功请求 | 5,532 | 0 | 100% 失败 |
| 故障表现 | 正常服务 | 全部 EOF | 服务实质不可用 |
关键细节:kubectl rollout status 报告 Pod 正常(进程存活),健康检查也能通过(轻量级请求),但所有业务请求返回 EOF——服务端接受了 TCP 连接,但 Tomcat 工作线程被 CFS 反复中断,无法在合理时间内完成 HTTP 响应。
这是 Kubernetes 中最难排查的故障模式:Pod 是"活着"的,但业务 100% 失败。健康检查告诉你一切正常,用户却在报障。
为什么 GC 停顿从 28ms 变成 318ms?#
G1GC 的并发 GC 线程和 JVM 工作线程共享 CPU 时间配额。在 250m CPU 限制下,Linux CFS 每 100ms 只给容器 25ms CPU 时间。GC 线程在标记和回收过程中被反复挂起,单次 Stop-The-World 停顿从 28ms 拉长到 318ms——即使堆配置正确也救不了。
Java 25 的改进:ActiveProcessorCount 自动修剪#
在旧版本 Java 中,如果你给容器限流 250m 但宿主机是 128 核,JVM 可能会错误地识别出核心数过多,从而开启上百个 GC 线程或庞大的 ForkJoinPool,导致剧烈的 上下文切换 (Context Switching),进一步拖垮性能。
Java 25 的黑科技:
它引入了更智能的调度感知。在场景 03 (250m) 的验证中,即使不手动设置 -XX:ActiveProcessorCount=1,Java 25 也能在启动瞬间通过 cpu.max 的时间切片分布,自动修剪其内部的并行线程数(这一机制在 JDK-8281181 引入 Cgroup v2 原生支持后得到了彻底优化)。这确保了即便资源受限,JVM 也不会为了“并行”而过度竞争 CPU。
问题三:Pod 规格——大副本 vs 小副本#
文章说相同总算力下,大副本优于多个小副本。实验结果(相同总算力 1.5 vCPU):
| 配置 | 单 Pod GC 最大停顿 | p95 延迟 | 最大延迟 |
|---|---|---|---|
| 3 × 0.5c 小副本 | 104 ms | 32 ms | 778 ms |
| 1 × 1.5c 大副本 | 31 ms | 20 ms | 256 ms |
| 改善 | -70% | -37% | -67% |
大副本的 GC 最大停顿降低 70%,p95 延迟降低 37%。原因和 CPU 节流一样:G1GC 的并发 GC 线程在 0.5c 下被限流,停顿拉长;在 1.5c 下有充足 CPU,停顿缩短。
实际部署建议:
| 场景 | 推荐 |
|---|---|
| 需要高可用(不可单点故障) | 多副本,但每副本 CPU ≥ 1c |
| 服务发现 / 边车模式 | 每 Pod CPU request ≥ 500m,limit ≥ 1000m |
| 弹性扩缩容 | 设合理的 HPA,基于 CPU 利用率而非固定副本数 |
最佳实践:部署 Java 25 on Kubernetes 的完整配置#
Deployment YAML 模板#
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-java-app
spec:
replicas: 2
template:
spec:
containers:
- name: app
image: my-java-app:latest
env:
# 2026 推荐配置:Guaranteed QoS + AlwaysPreTouch
- name: JAVA_TOOL_OPTIONS
value: "-XX:InitialRAMPercentage=75.0 -XX:MaxRAMPercentage=75.0 -XX:MinRAMPercentage=75.0 -XX:+AlwaysPreTouch -XX:+ExitOnOutOfMemoryError"
resources:
requests:
memory: "1Gi"
cpu: "1000m" # 2026 时代,1.0 CPU 应该是微服务起点
limits:
memory: "1Gi"
cpu: "2000m" # 允许突发峰值,但不建议设过低
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30 # 给 JVM 足够启动时间
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
Spring Boot application.properties#
# 暴露 liveness 和 readiness 探针
management.endpoints.web.exposure.include=health,metrics,info
management.endpoint.health.show-details=always
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true
# 启用虚拟线程(Java 21+)
spring.threads.virtual.enabled=true
JVM 参数说明#
# 核心三剑客(解决内存浪费和扩容开销问题)
-XX:MaxRAMPercentage=75.0 # 容器内存 > 250MB 时,最大堆占容器比例(业界稳健标准)
-XX:InitialRAMPercentage=75.0 # 初始堆占容器比例(与 Max 相等实现 Guaranteed QoS)
-XX:MinRAMPercentage=75.0 # 容器内存 < 250MB 时,最大堆占容器比例
# 启动加速与性能抖动防御
-XX:+AlwaysPreTouch # 启动时预先触碰物理页,减少运行时首次访问 Page Fault 导致的延迟
# 退出机制(OOM 时主动退出,触发 K8s 重启而不是假活)
-XX:+ExitOnOutOfMemoryError
注意:在 2026 年,请彻底移除脚本中的
-Xmx和-Xms。如果在JAVA_TOOL_OPTIONS中混用了-Xmx,百分比参数将被静默忽略,这可能导致你的服务在发布后因资源限制误判而 OOM。
如何验证你的配置是否正确#
加上 Spring Actuator 后,查询 jvm.memory.max(最大堆)和 jvm.memory.used(初始堆在启动后应立即达到最大值的接近水平):
# 查看堆上限(应该是容器内存的 75%)
curl http://localhost:8080/actuator/metrics/jvm.memory.max?tag=area:heap
# 查看 GC 停顿统计
curl http://localhost:8080/actuator/metrics/jvm.gc.pause
# 或者直接在容器启动日志里找
# 加了参数后,启动时会打印:
# Heap space: ...(约 75% 的容器内存)
总结#
| 问题 | 是否属实 | 修复方案 | 难度 |
|---|---|---|---|
| 默认堆只有容器内存 25% | ✅ 完全属实 | -XX:MaxRAMPercentage=75.0 |
极低 |
| 初始堆过小导致启动慢 | ✅ 属实 | -XX:InitialRAMPercentage=75.0 |
极低 |
| CPU 不足导致性能崩溃 | ✅ 属实且更严重 | CPU limit ≥ 500m,推荐 ≥ 1000m | 中 |
| 大 Pod 优于多小 Pod | ✅ 属实 | 每副本 CPU ≥ 1c,保持单副本资源充足 | 中 |
最重要的一行配置:
JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=75.0 -XX:MaxRAMPercentage=75.0 -XX:MinRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
成本:零。收益:内存利用率从 25% 提升到 75%,省去启动时的堆扩容开销,并增强了对小规格容器的自适应性。任何 Java on Kubernetes 的应用都应该加。
实验代码和完整数据:java25-config-in-k8s