现象:容器 4 核,GC 却开了 24 个线程
八月初一个 Pod 频繁被 K8s 驱逐,OOMKilled 日志看着是内存超了,但监控显示内存没到 limit。我进容器一查 CPU 使用率 400%——而这 Pod 只申请了 4 核。根因是 GC 线程数按宿主机 24 核算的,Parallel GC 把 24 个回收线程全拉起来抢 CPU,触发了 CPU throttle 导致的连锁反应。
根因:旧 JDK 不感知容器限额
在 JDK 8u191 之前,JVM 读的是宿主机的 Runtime.getAvailableProcessors(),容器里的 --cpus=4 限制完全被无视。GC 线程数按这个公式算:
ParallelGCThreads = (ncpus <= 8) ? ncpus : 8 + (ncpus - 8) * 5 / 8
宿主机 24 核代入,得到 8 + 16*5/8 = 18,再加 ConcGC 线程,轻松超过容器核数。结果 GC 时 18~24 个线程同时跑,把 4 核的 CPU quota 打爆,任务被 throttle,吞吐反而更差。
容器感知:让 JVM 认得限额
JDK 8u191+ / 11+ / 17 默认开启容器感知(-XX:+UseContainerSupport)。只要 JVM 启动参数里不设 -XX:ParallelGCThreads,它会读 cgroup 的 cpu quota / period 来定 ncpus。验证一下:
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E "ParallelGCThreads|MaxRAM"
# 在 4 核容器里应看到 ParallelGCThreads ≈ 4
我们那个 Pod 之所以翻车,是因为启动脚本里硬编码了 -XX:ParallelGCThreads=24(早年从物理机搬来的遗留参数),把容器感知覆盖掉了。
手动调优场景
大多数情况让 JVM 自动算就行,但两种情形要手动干预:
- GC 线程过多拖慢业务:容器 8 核但业务是低延迟型,可把
ParallelGCThreads压到 4,给业务线程留 CPU。代价是单次 GC 更慢。 - CPU 超分环境:云上经常
requests=2但limits=8,JVM 按 8 算线程,实际被限到 2 核,反而劣化。这种要按 requests 而非 limits 设:
-XX:ParallelGCThreads=2 \
-XX:ConcGCThreads=1
配套:内存也得设对
容器感知同样管内存,但 -Xmx 不指定时 JVM 按 MaxRAM 的 25% 设堆,可能超过容器 limit。我们统一在部署模板里显式写:
-XX:+UseContainerSupport \
-Xmx1500m -Xms1500m \
-XX:ParallelGCThreads=4
改完后该 Pod CPU 峰值从 400% 回到 380%(接近 4 核满负荷但不再 throttle),再没被驱逐。
写在后面
现在回头看,《GC 线程数配置与容器 CPU 限制的关系》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。