本文涉及的主要环境与基础组件版本如下:
- JDK:17
- 容器环境:Docker / Kubernetes
- 垃圾回收器:G1, ZGC
问题定义
JDK 版本升级后并且把垃圾回收器从 G1 切换至 ZGC 的过程中,我观察到了一个违背直觉的监控现象。当 Pod 的容器内存上限(Limit)严格限制在 2GB 时,通过传统的系统级命令探查进程状态,原先使用 G1 的服务 RSS(Resident Set Size,驻留内存)稳定在 1.5GB 左右;而切换至 ZGC 的服务,其 RSS 读数竟飙升至 3.5GB,不仅远超 2GB 的硬性阈值,甚至达到了不可思议的比例。
反常的是,尽管操作系统的 RSS 统计显示内存严重超额,容器却并未因此触发 OOM (Out Of Memory) 甚至发生任何崩溃。为了消除这一严重的监控误报,并确保在生产环境大规模落地 ZGC 的安全性,我决定对底层内存映射与 Linux 的统计机制进行深入排查。
分析与诊断过程
查阅 ZGC 的底层规范与 Linux 内存管理机制后证实,这一读数异常纯粹是操作系统统计口径偏差所导致的“障眼法”。
在使用 G1 回收器时,内存的申请遵循传统的线性映射关系,即一份物理内存严格对应一份虚拟内存。因此,通过读取 /proc/$PID/smaps 聚合出的 RSS 值,基本能够准确反映真实的物理内存占用。
而在 ZGC 的架构设计中,为了达成亚毫秒级的并发标记与转移,其核心引入了染色指针(Colored Pointers)机制。为了在 Linux 内核上支撑这一特性,ZGC 采用了多重映射( Multi-mapping)技术。它将同一块底层的物理内存,同时映射到了三个独立的虚拟地址空间(Marked0、Marked1、Remapped)。
由此导致的结果是:当 top 或针对 /proc/$PID/smaps 的标准监控脚本进行 RSS 聚合运算时,内核将同一块共享物理内存重复累加了三次。这正是 3.5GB 这一虚高数据的根源。
为了验证这一点,并构建准确的监控采集方案,我对基于进程层面和 Cgroup 层面的采集链路进行了重构。
G1 回收器的观测基准点
由于 G1 的行为符合传统认知,针对其的测量脚本被我用作本次对比的基线。
在容器内部提取 Java 进程 PID 并累加 smaps 中 Rss 字段的基线脚本:
1 | # 进入容器提取 PID,统计真实物理内存的 RSS 映射量 |
在容器层级(Cgroup),为了精准规避文件系统 Page Cache 的影响,需要直接观测匿名内存占用:
1 | # 适用于 Cgroup V1 环境,读取 total_rss 排除 Cache 干扰 |
ZGC 回收器的测量机制修正
明确了 ZGC 的多重映射机制后,我废弃了基于 smaps Rss 的监控方案。在进程粒度上,我转向采集 PSS (Proportional Set Size) 指标。PSS 的计算内核能够识别多重映射行为,并进行等比例摊销(即映射三次,该物理页的记账值除以三),从而还原接近真实的物理内存消耗。
调整后的 ZGC 进程级内存探测脚本:
1 | # 聚合 PSS 指标替代 RSS,以对消染色指针造成的数据放大 |
深入至 Cgroup 层级时,我捕获到了另一处统计陷阱:ZGC 借助 Linux 共享内存(Shmem / memfd)机制落地多重映射。在 Cgroup V2 的统计划分中,共享内存被硬性归类为文件映射(File),而不再计入匿名内存(Anon)。如果监控大屏仅盯死 anon 曲线,就会发生严重的内存漏算。
纠正后的 Cgroup V2 测量脚本联合了 anon 与 shmem:
1 | # 必须聚合匿名内存与共享内存,方可计算出 ZGC 的真实物理驻留量 |
无论 G1 还是 ZGC 都适用的 NMT
如果你不想和 Linux 复杂的 Cgroup 版本和内存统计口径斗智斗勇,最准确、最权威的方法是让 JVM 自己汇报内存占用。
JVM 的 Native Memory Tracking (NMT) 是从内部视角进行统计的,它完全不受操作系统多重映射或共享内存分类的影响。
操作步骤:
- 在 Java 启动参数中开启 NMT:
1 | -XX:NativeMemoryTracking=summary |
- 在应用运行期间,进入容器执行
jcmd命令查看总览:
1 | jcmd <PID> VM.native_memory summary |
另外NMT还可以根据基线查看内存变化:
1 | jcmd <PID> VM.native_memory baseline scale=MB |
总结对比表
| 排查维度 | G1 回收器 | ZGC 回收器 |
|---|---|---|
| Linux PID 级别统计 | 直接看 /proc/$PID/smaps 的 RSS |
必须看 /proc/$PID/smaps 的 PSS (看 RSS 会翻倍虚高) |
| Cgroup 容器防 OOM | V1 看 total_rss,V2 看 anon |
V1 看 total_rss,V2 必须看 anon + shmem |
| JVM 内部精准统计 | 开启 NMT 看 committed |
开启 NMT 看 committed |