你写了一个Java服务,跑着几百万个对象,GC和内存占用一直是头疼的事。Java 27带来一个号称能压缩对象头的特性——Project Lilliput(JEP 534,即Java增强提案,正式将对象头从12字节压到8字节)。按理说每个对象省4字节,上千万对象就能省几十MB,但当你真的开启这个开关,盯着操作系统的RSS(常驻内存集,进程实际占用的物理内存)看,数字几乎没动。这到底是特性没用,还是你观察的层面不对?JetBrains的团队在IntelliJ IDEA上做了场真刀真枪的实测,结论很能说明问题。
他们用了三个场景:只开欢迎页、一个小项目(Spring Petclinic,约3370行代码)和一个大项目(IntelliJ IDEA自己的monorepo,约2650万行代码)。同一个IDE版本、同一个JVM(JBR 25.0.4),手动把紧凑对象头打开和关闭,各跑多轮,连执行顺序都交替着来,排除缓存和排序偏差。结果先看进程RSS,三个场景里,开启紧凑头后内存变化分别是:欢迎页反而涨了39.5 MiB(+3.22%),小项目降了44.7 MiB(-1.25%),大项目涨了24.3 MiB(+0.33%)。这些波动都在噪声范围内,统计上根本没法证明有改善。
那省下的字节去哪了?要回答这个,得把视线从进程拽到堆内。RSS是整块物理内存,包含堆里还没用上的保留空间、类元数据、JIT编译的代码、线程栈、本地缓冲区、共享库……对象头只是沧海一粟。团队用JOL(Java对象布局工具,专门分析对象内部字段偏移和填充的工具)对比同一批对象在两种头模型下的足迹——也就是这些对象本身占多少字节(不算引用的其他对象,叫浅大小)。结果清清楚楚:欢迎页的258万个实例,从141.533 MiB降到132.619 MiB,省了6.3%;小项目933万个实例省6.3%;大项目1910万个实例省8.2%,开启后1998万个实例省8.4%。堆内节省是实打实的,但进程级RSS把这点红利吞掉了。
为什么省4字节没变成每个对象省4字节?因为HotSpot默认把每个对象的浅大小按8字节向上取整。一个只有int字段的对象,12字节头+4字节字段=16,换成8字节头+4字节字段=12,但还得补到16,等于没变;而空Object从16变8,两个int字段的对象从24变16。字段的偏移和填充也会“吃掉”节省:比如Long.value(八个字节的字段),12字节头后面跟8字节字段,字段得从偏移16开始(中间空4字节),整个对象24字节;换成8字节头后,字段可以从偏移8开始,对象缩到16。数组也一样,byte[0]还是16,byte[1]从24变16。堆里几十万种形状混在一起,总的节省取决于这些形状的比例,不是单纯看对象数量。
这个测试最让人意外的地方在于:你问“系统内存降没降”,答案是“没有”;但问“堆内对象占的空间降没降”,答案是“降了6%~8%”。真相是,对IntelliJ IDEA这种重量级IDE,堆外内存和未分配空间占了RSS的大头,对象头压缩省下的那点堆内空间,在进程层面被稀释得无影无踪。
这给普通开发者什么启发?第一,如果你的应用受堆内存限制(比如容器内存配额、频繁Full GC),那么压缩对象头值得开——Java 27默认开启,老版本可以用-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders实验性打开。第二,评估优化效果别只看任务管理器里的内存数,要看堆内实际存活对象的占用,用JOL或IDEA自带的内存Dump(2026.3 EAP起还支持按紧凑头模型估算)去分析。第三,对象形状决定收益:全是空对象、长整型字段的类能明显瘦身,一个int字段的类白费力气。如果你在写大规模缓存、事件流、元数据模型,趁早把对象布局按8字节对齐和字段重排考虑进去,这才是把内存抠到极致的正路。

内容与图片版权归原作者所有 · 原文: https://blog.jetbrains.com/idea/2026/11/where-did-the-saved-bytes-go-compact-object-headers-in-intellij-idea/