jstat观察老年代实际占用率应盯OU字段,长期维持60%~80%为健康,持续超90%预示Full GC风险;OU=老年代已用字节数/总容量,反映GC后真实存活对象集合。

用 jstat 观察老年代实际占用率,不是看堆总量
堆大小不是靠“对象数量 × 平均大小”拍脑袋算出来的。真正决定是否够用的,是老年代在稳定业务压力下的实际占用水位。jstat -gcutil <pid></pid> 每秒采样,盯住 OU(Old Used)字段:长期维持在 60%~80% 是健康区间;持续高于 90% 就说明对象在老年代堆积,很快会触发 Full GC。
注意:OU 是已用字节数 / 老年代总容量,不是“所有对象加起来有多大”。它反映的是 GC 后仍存活、且没被晋升或回收的对象集合——这才是你该对齐的真实负载。
用 JOL 测关键类实例的 shallow size,别信理论值
同一个 Person 类,在 JDK 17 + CompressedOops 开启时,new Person() 可能占 24 字节;关掉压缩指针就变成 32 字节。差的不只是 8 字节,而是乘以百万级对象后,直接多占近百 MB 堆空间。
实操建议:
- 用
ClassLayout.parseInstance(new Person()).toPrintable()看真实字段偏移和填充 - 用
GraphLayout.parseInstance(obj).totalSize()获取含数组/嵌套对象的总浅层大小 - 重点测高频创建的 DTO、VO、缓存 key/value 类,忽略日志对象、临时包装类等低频样本
结合 Survivor 区晋升率反推对象生命周期
如果 -XX:SurvivorRatio=8 下,每次 Minor GC 后 S0U 或 S1U 都快速涨到 95% 以上,说明大量对象活过一次 GC,正在向老年代“滑坡”。这时增大新生代未必有用,反而可能因复制成本上升拖慢 GC;更该做的是检查这些对象是否真需长期存活——比如缓存未设过期、监听器未注销、Stream 中间对象未及时释放。
关键指标是 YGC 和 YGCT 的比值:如果 YGCT 占总 GC 时间(GCT)比例超过 40%,说明新生代压力已成瓶颈,光调堆大小解决不了根本问题。
容器环境必须验证 JVM 是否读取了 cgroup 内存限制
OpenJDK 8u222+ 默认从 /sys/fs/cgroup/memory/memory.limit_in_bytes 读取内存上限,但旧版本(如 8u131)需要显式加 -XX:+UseCGroupMemoryLimitForHeap。不验证这点,你在 512MB 容器里跑出 -Xmx2g,结果就是 OOM Killer 直接杀进程,而不是 JVM 报 OutOfMemoryError。
验证方法很简单:
- 启动时加
-XX:+PrintGCDetails,看日志里 Initial Heap Size 和 Max Heap Size 是否符合预期 - 运行中执行
jinfo -flag MaxHeapSize <pid></pid>,输出值应接近容器内存限制的 50%~70% - 若输出是 1.7G 之类明显超出容器限制的数字,立刻检查 JDK 版本和是否漏配参数
OU 上,但会让 EU(Eden Used)居高不下、Minor GC 频繁。所以观察堆,永远要带着 GC 日志和对象分布一起看。


















