Java 25 has a flag that shrinks every object header, and it is off

by Kaushal Bhatt
4 min read

Java 25 shipped a flag that cuts every object header by a third. It is still off by default. If you upgraded and changed nothing else, you are not getting it. ## What the header costs you Every object on the JVM heap carries a header — a mark word plus a class pointer. On 64-bit that is 96 bits, or 128 for arrays. For a typical object in the 256–512 bit range, the header alone is over 20% of the footprint. You are paying it on every single allocation, and it is invisible in every profiler view that reports by class. JEP 519 collapses it to 64 bits: the class pointer is compressed to 22 bits and folded into the mark word. What OpenJDK measured: - SPECjbb2015: 22% less heap, 8% less CPU time - 15% fewer GC cycles on G1 and Parallel - A parallel JSON parser benchmark: 10% faster One flag:


-XX:+UseCompactObjectHeaders

In Java 24 this was experimental (JEP 450) and needed UnlockExperimentalVMOptions. In 25 it is a product feature. ## The part the release-note posts leave out It does not work with ZGC. G1 and Parallel only. It is silently disabled when JVMCI is enabled — so if you are running Graal JIT, you get nothing, and no warning that you got nothing. Compressed class pointers become mandatory, which caps you below roughly four million loaded classes. Fine for every application I have worked on, but it is a real ceiling and worth knowing it exists. The legacy stack-locking path is removed entirely. If you run an agent or a profiler that reads object headers directly, test it before this reaches production. ## How to decide The gain is real and it costs you zero lines of code. It is not free in the sense that you flip it and walk away. Measure it against your actual object graph. Workloads with many small objects see the most — a service holding mostly large byte arrays will see close to nothing, because the header is already noise next to the payload. That is the general shape of every JVM flag worth turning on: the benchmark tells you the ceiling, and your allocation profile tells you how much of that ceiling is available to you. The two are rarely the same number. If you are on 25 and G1, what is the actual blocker to testing this in staging this sprint?