Object Layout and Footprint
Purpose
Answer "what will N of these cost" before N of them exist, and read a layout measurement
without being misled by it.
The failure this prevents is the confident a-priori estimate — header + fields, times a
population — which omits alignment, omits the array length field, assumes compact object
headers save eight bytes per object, and is quoted without the JDK build or the header mode
that produced it. Every one of those errors is directional: they all understate the real
footprint, except the compact-header one, which overstates the saving on exactly the objects
that dominate a real heap.
The gate
No size without three things attached: the JDK build, the tool, and the header mode.
Inspect the project's toolchain, deployed JVM, collector, heap size and effective alignment/
compression flags before applying these HotSpot measurements. They do not authorize a Java
upgrade, dependency addition or flag change. Historical [executed] tables below record the
stated experiments; they are not a claim that every applying agent reran those experiments.
A pasted ClassLayout listing with no command line is unusable, not merely incomplete. The
same class measures 32 or 24 bytes and the same array measures 24 or 16, on one JVM, decided
by one flag. On JDK 27 that flag's default flips [source-only: JEP 534], so an unlabelled
listing does not even tell you which of two answers it is.
State it as 48 bytes (Temurin 25.0.3+9, JOL 0.17 ClassLayout.instanceSize, default headers)
or do not state it.
The arithmetic
This is the whole method. It is version-scoped: executed on Temurin 21.0.12+8 (Linux x64),
25.0.3+9 (Windows x64) and 26.0.2+10 (Linux x64), all three agreeing, with JOL 0.17
cross-checked against Instrumentation.getObjectSize.
instance = alignUp( header + Σ field sizes , ObjectAlignmentInBytes )
array = alignUp( arrayBase + n × elementSize , ObjectAlignmentInBytes )
For the ordinary, non-@Contended HotSpot layouts tested here, Σ field sizes is the plain
sum of declared and inherited non-static fields. Do not promote this measured model to a JVM
specification: VM-injected fields, special classes, value-class experiments, alignment flags
and future layout algorithms require a target-build measurement. Holes are an output of the
layout, not a portable input. The model matched 650 generated classes in both tested header
modes [executed] — 0–20 random fields, 2–4-deep inheritance chains, zero misses.
| Term |
Classic headers |
Compact object headers |
header (instance) |
12 — mark 8 + compressed klass 4 |
8 — one fused word |
arrayBase, elements ≤ 4 B |
16 — 12 + a 4-byte length field |
12 |
arrayBase, 8-byte elements |
16 |
16 — the 4 freed bytes become a pad |
ref (a reference field) |
4 with compressed oops, 8 without |
same, both modes |
ObjectAlignmentInBytes |
8 |
8 |
Other field sizes, executed, all modes: boolean 1, byte 1, char 2, short 2, int 4,
float 4, long 8, double 8.
The base tables use compressed oops; explicit wide-reference tables are exceptions — the second precondition,
and the one that changes without a flag. Ergonomics commonly turns them off near 32 GB at
8-byte alignment, and
the boundary is below how it is usually quoted: measured on 25.0.3,
-Xmx32736m still gives UseCompressedOops = true {ergonomic} and
-Xmx32740m already gives false {default} — so -Xmx32g is off, not the last value on.
-Xmx31g on / -Xmx32g off reproduces on 26.0.2 [executed]. The margin is the heap
alignment, so it moves with collector and page size; the boundary itself scales with
ObjectAlignmentInBytes — at 16, -Xmx60g is on and -Xmx64g off [executed]. Past it,
UseCompressedOops reads false {default}, not {ergonomic}, so read the value and not
the origin; -Xlog:gc+init prints Compressed Oops: Enabled (32-bit) or Disabled
(references/production-footprint-checks.md §2). Past that boundary recompute
p with
ref = 8; the rule is unchanged but its answers move, including which classes save. Five
of the fourteen rows in compact-object-headers.md §2 reverse, and Object[] becomes an
8-byte-element array that shrinks by nothing at any length. That matters here because a
population large enough to ask this skill's question is often a heap large enough to cross the
threshold. Where the threshold is as a heap-sizing decision is jvm-performance-review's.
Which header column is in force: classic on JDK 21, 25 and 26 [executed]; compact on JDK 27
[source-only: JEP 534, Closed / Delivered, Release 27] — JDK 27 is not GA and nothing in
this skill was run on it. The flag is experimental on 24 and needs
-XX:+UnlockExperimentalVMOptions there (JEP 450); a product flag on 25 (JEP 519, executed:
no unlock needed); default on 27 (JEP 534). A JDK 24 command line pasted onto 25 works; a 25
line pasted onto 24 does not. The rest of the flag's lifecycle and its cost belong to
jvm-performance-review.
Three things the arithmetic gets wrong if you stop before the alignUp:
record Point(int,int) computes to 12 + 8 = 20 and is 24.
byte[1] computes to 17 and is 24. So do byte[2] through byte[8].
- Declaration order is not the layout. Fields are grouped by descending size with
references placed last, and under classic headers a 4-byte field is hoisted into the
12–15 header hole ahead of the 8-byte group. You cannot compute an offset from source
order; you can compute a size.
Workflow
Pin the header mode before anything else, on the target build — never assumed from the
release number, and never read off the command line. The flag can read true where it was
passed and false where it runs: two conditions cause that on 25.0.3 — disabled compressed
class pointers, and a heap larger than 8191 GB with header-based forwarding — each
announced only by a one-line warning on stderr that nobody reads.
java <same target flags> -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders
jcmd <pid> VM.flags -all | grep UseCompactObjectHeaders # already running
-all is not optional: plain VM.flags prints nothing at all for a JVM sitting at the
default, and the one character that carries the answer — the +/- — is the first thing a
grep -o throws away. Read references/compact-object-headers.md §4 whenever
-XX:+UseCompactObjectHeaders appears anywhere in the artefact — it has the three origin
tags to match on. Why the JVM overrode it, and what that means for the rest of the
configuration, is jvm-performance-review's.
Compute the per-element size a priori with the arithmetic above. For arrays, for the
per-length table, and for the superclass gap-filling rule, read
references/array-and-object-arithmetic.md. Do this before measuring: a prediction that
the measurement then confirms is worth far more than a measurement alone, because it is
the prediction that transfers to the next class.
Decide whether compact object headers change the answer. They are not a uniform
8-byte saving and they are zero on several of the commonest classes in a Java heap. Read
references/compact-object-headers.md for the measured per-class table and the rule that
predicts each row. Never multiply 8 bytes by an object count.
Only now compare shapes. Record versus final class versus primitive array versus
parallel arrays versus a boxed collection, at the stated population size, in measured
bytes per element. Read references/shape-decision.md. Emit bytes per element and the
total at N, in both header modes when supported; otherwise label the alternate model
hypothetical rather than changing the project baseline. Never report a percentage alone.
Measure to confirm the prediction. Read references/jol-operating-procedure.md for
the invocation that works on JDK 25/26, the four ways JOL fails — one of which throws on
the first record you try — and the Instrumentation.getObjectSize cross-check. Read it
before the first JOL run, not after the first stack trace. When the population already
lives in a JVM you cannot attach JOL to, jcmd <pid> GC.class_histogram reports shallow
sizes computed by that JVM in its own header mode — Point 24 → 16 [executed] — and
references/production-footprint-checks.md §1 says what a heap dump cannot tell you.
Report with the gate satisfied. Build, tool, header mode, and shallow versus deep
stated explicitly. If the number came from a source rather than a run, label it as
source-derived; JDK 27 is not GA and nothing about it here was executed.
The headline: the record-versus-array intuition is backwards
For a four-long payload, measured on Temurin 25.0.3+9 and 26.0.2+10, JOL 0.17 agreeing
with Instrumentation.getObjectSize:
| Shape |
Arithmetic (classic) |
Classic |
Arithmetic (compact) |
Compact |
record Rec4(long×4) |
12 hdr + 32 → align8 |
48 |
8 hdr + 32 → align8 |
40 |
long[4] |
16 base + 32 → align8 |
48 |
16 base + 32 → align8 |
48 |
Under classic headers they tie: the record's 4-byte header hole exactly cancels the
array's 4-byte length field. Under compact object headers the record wins by 8 bytes,
because 8-byte elements must stay 8-byte aligned, so long[] spends the freed header bytes
on a pad and shrinks by nothing at any length.
"Drop the record for a primitive array to save the header" is therefore wrong for a
four-long payload today and more wrong once compact headers are the default. The
direction of the comparison changes, not just its magnitude — which is why a footprint
optimisation justified on JDK 25 must be re-measured before it ships on JDK 27.
The intuition is only right when the array amortises one header across many elements.
Parallel primitive arrays beat a million records by 20 bytes each — 24.00 against 44.00 bytes
per element, measured (shape-decision.md §1). One long[4] beats a single Rec4 by
nothing. That distinction — replacing N headers, not replacing one — is the break-even, and
it is the point of that reference.
Rules
- Treat compact object headers as a measured deployment decision. Answer
only the footprint half: the saving is workload-specific and frequently zero, so quote it
from the class mix, and say what would prove it — the same live-set measurement in both
modes on the same build (heap after a verified collection, or
GraphLayout.totalSize() over the actual
population), never an object count times eight. What the flag costs, its prerequisites,
its lifecycle and its per-release defaults are jvm-performance-review's; route there
rather than summarising.
- Distinguish the boxed type, collection structure and reference width.
Under compressed oops
Integer, Boolean, ArrayList and the String object are all
unchanged, and ArrayList<Integer> of 1000 distinct values measured 20,976 bytes in both
modes on 25.0.3 and 26.0.2. At 32 GB and above the same population measures 25,920 → 25,912 — the
ArrayList itself now saves 8 bytes and nothing else does, so the conclusion survives but
the equality does not. A boxed-collection-heavy heap is the case where an "8 bytes per
object" plan overstates the saving. This is not a rule for all boxed collections:
Long/Double boxes and, with compressed oops, map nodes do shrink in the tables.
- State the encoding — and the oop size — before making any claim about a string. Under
compressed oops the
String object is 24 bytes in both modes at every length, so only its
byte[] payload can shrink and the rule runs over payload bytes: 8 when
(length × bytesPerChar) mod 8 ∈ {1,2,3,4}, else 0. With COMPACT_STRINGS on by default,
bytesPerChar is 1 for a Latin-1-representable string and 2 for anything containing a
character above U+00FF, and the two encodings give opposite answers at 3–6 characters:
"5 to 8 characters gains nothing" is true for ASCII and false for UTF-16 at 5–6. At 32 GB and above
the String object itself goes 32 → 24, so it saves 8 regardless of payload and the whole
example inverts — String[1000] of 8-character strings goes from a flat 52,016 to
64,016 → 56,016, a 12.5% saving [executed]. compact-object-headers.md §3 has every
measured column.
- Shallow is not deep, and the gap is the whole answer for anything holding references.
ClassLayout.instanceSize() on new String("EUR") is 24 bytes; GraphLayout.totalSize()
is 48. Name which one you measured, every time.
- Version-scope every size, and label anything not executed. JDK 27 is not GA. Its
default header mode is read from JEP 534 (
Closed / Delivered, Release 27, confirmed at
openjdk.org/jeps/534), not observed. -XX:+UseCompactObjectHeaders does not exist at all
on JDK 21 — the JVM refuses to start with Unrecognized VM option (executed, 21.0.12+8).
- Match sharing to the question. To model distinct boxes, verify values are outside the
configured
Integer cache (which can exceed 127). To model a real cached population,
retain its sharing: JOL counts each reachable box once, not once per reference. Reachable
bytes are not incremental allocation or retained bytes. See the JOL procedure's array-root trap.
- A per-object saving is not a heap saving until it is multiplied by the live population.
Eight bytes off
HashMap$Node saves about 8 KB at a thousand entries and 320 MB at forty
million. Compare with the required headroom; ask for N before estimating a total.
- Do not reach for
-XX:+PrintFieldLayout. It is a develop flag: on the tested production
JDK the JVM refuses to start with it (executed, 25.0.3), there is no -Xlog equivalent,
and JOL is the tool used here on production builds. Direct VM/offset tooling is another
option; consult the JOL reference for the source-only change on JDK 28 development builds.
- Compact object headers buy heap bytes with class-space bytes. The 22-bit class
pointer used a shift of 10 in the measured configuration: 537 → 1,024 bytes per class of
compressed class space (executed, 25.0.3, 100,000 strong hidden classes), so the 1 GB
default holds ~1.06 M classes instead of ~2 M. On a proxy- or lambda-heavy service read
jcmd <pid> VM.metaspace before switching, and quote both sides.
references/production-footprint-checks.md §3.
- Under G1, an object whose aligned size exceeds half a region reserves whole regions.
byte[600000] requires one 1,048,576-byte region with 1 MB regions;
byte[1100000] requires two. The historical aggregate heap deltas in the production
reference are estimates, not exact object sizes or histogram charges.
The per-object arithmetic is exact and the heap cost is still wrong by up to a region per
array — size chunks against G1HeapRegionSize.
references/production-footprint-checks.md §4.
- Hashing and locking did not change shallow object size on the tested JDK 25 build, in
either header mode (executed):
the identity hash and the monitor state live in the mark word or beside the object. Do
not carry "hashed objects get bigger" from other or proposed header layouts into this build.
References
- Array and object arithmetic — the header
composition, the length field, element alignment, the per-length size table for
byte[]/int[]/long[]/Object[] in both modes, the measured field-ordering and
superclass gap-filling rules, and what ObjectAlignmentInBytes=16 costs per object. Read
at step 2, whenever a size is being computed rather than measured.
- Compact object headers, measured — which objects
shrink and which do not, with the rule that predicts every row; the two conditions that
disable the flag while it still reads as set; and the deep-footprint tables showing where
the saving is zero. Read at step 1 whenever the flag appears, and at step 3 always.
- The shape decision — record versus final class versus
primitive array versus parallel arrays versus
ArrayList versus HashMap, measured at
N = 1,000,000 in bytes per element under both header modes, with the break-even reasoning
and the costs that are not bytes. Read at step 4.
- JOL operating procedure — the exact invocation
for JDK 25/26, the four failure modes with their verbatim messages and fixes, and the
two-file
Instrumentation.getObjectSize agent that cross-checks JOL rather than trusting
it. Read at step 5, before the first run.
- Production footprint checks — sizes the
running JVM reports without JOL (
GC.class_histogram, jdk.ObjectCount) and why a heap
dump is not one of them; the compressed-oops boundary by alignment and the origin-tag
trap; the 1 KB-per-Klass class-space cost of compact headers; G1 humongous rounding
for large arrays; what never changes an object's size; and the symptom-to-cause table for
a prediction that disagrees with an observation. Read at step 5 when JOL cannot be
attached, and at step 6 whenever the numbers disagree.
Authoritative sources for release-sensitive claims:
1---2name: object-layout-and-footprint3description: Sizing a data structure in bytes before it exists. Use when a shape is chosen for millions of instances — record, class, primitive array, parallel arrays or boxed collection; when an array is proposed to save the header; when HashMap<Integer,Integer> or List<Long> is on a bulk path; when -XX:+UseCompactObjectHeaders is evaluated for footprint; or when smaller objects are expected to buy shorter GC pauses without a collector-specific measurement. Answers in bytes per element; one record-versus-array example reverses under the JDK 27 default (JEP 534, not yet GA). Sizing a replacement belongs here; measuring what exists is heap-dump-analysis. Not flag lifecycle (jvm-performance-review), @Contended padding (false-sharing-and-contended), cache hierarchy (cpu-cache-and-numa), allocation rate (allocation-profiling), container budget (jvm-memory-regions), compressed class space (metaspace-internals), off-heap memory (off-heap-memory), or sharing duplicates (gof-flyweight).4---56# Object Layout and Footprint78## Purpose910Answer "what will N of these cost" before N of them exist, and read a layout measurement11without being misled by it.1213The failure this prevents is the confident a-priori estimate — `header + fields`, times a14population — which omits alignment, omits the array length field, assumes compact object15headers save eight bytes per object, and is quoted without the JDK build or the header mode16that produced it. Every one of those errors is directional: they all understate the real17footprint, except the compact-header one, which overstates the saving on exactly the objects18that dominate a real heap.1920## The gate2122**No size without three things attached: the JDK build, the tool, and the header mode.**2324Inspect the project's toolchain, deployed JVM, collector, heap size and effective alignment/25compression flags before applying these HotSpot measurements. They do not authorize a Java26upgrade, dependency addition or flag change. Historical `[executed]` tables below record the27stated experiments; they are not a claim that every applying agent reran those experiments.2829A pasted `ClassLayout` listing with no command line is unusable, not merely incomplete. The30same class measures 32 or 24 bytes and the same array measures 24 or 16, on one JVM, decided31by one flag. On JDK 27 that flag's default flips `[source-only: JEP 534]`, so an unlabelled32listing does not even tell you which of two answers it is.3334State it as `48 bytes (Temurin 25.0.3+9, JOL 0.17 ClassLayout.instanceSize, default headers)`35or do not state it.3637## The arithmetic3839This is the whole method. It is version-scoped: **executed on Temurin 21.0.12+8 (Linux x64),4025.0.3+9 (Windows x64) and 26.0.2+10 (Linux x64), all three agreeing**, with JOL 0.1741cross-checked against `Instrumentation.getObjectSize`.4243```text44instance = alignUp( header + Σ field sizes , ObjectAlignmentInBytes )45array = alignUp( arrayBase + n × elementSize , ObjectAlignmentInBytes )46```4748For the ordinary, non-`@Contended` HotSpot layouts tested here, `Σ field sizes` is the plain49sum of declared and inherited non-static fields. Do not promote this measured model to a JVM50specification: VM-injected fields, special classes, value-class experiments, alignment flags51and future layout algorithms require a target-build measurement. Holes are an output of the52layout, not a portable input. The model matched 650 generated classes in both tested header53modes `[executed]` — 0–20 random fields, 2–4-deep inheritance chains, zero misses.5455| Term | Classic headers | Compact object headers |56| ---------------------------- | ----------------------------------------- | --------------------------------------- |57| `header` (instance) | **12** — mark 8 + compressed klass 4 | **8** — one fused word |58| `arrayBase`, elements ≤ 4 B | **16** — 12 + a 4-byte length field | **12** |59| `arrayBase`, 8-byte elements | **16** | **16** — the 4 freed bytes become a pad |60| `ref` (a reference field) | **4** with compressed oops, **8** without | same, both modes |61| `ObjectAlignmentInBytes` | 8 | 8 |6263Other field sizes, executed, all modes: `boolean` 1, `byte` 1, `char` 2, `short` 2, `int` 4,64`float` 4, `long` 8, `double` 8.6566**The base tables use compressed oops; explicit wide-reference tables are exceptions** — the second precondition,67and the one that changes without a flag. Ergonomics commonly turns them off near **32 GB** at688-byte alignment, and69the boundary is below how it is usually quoted: measured on 25.0.3,70`-Xmx32736m` still gives `UseCompressedOops = true {ergonomic}` and71`-Xmx32740m` already gives `false {default}` — so `-Xmx32g` is **off**, not the last value on.72`-Xmx31g` on / `-Xmx32g` off reproduces on 26.0.2 `[executed]`. The margin is the heap73alignment, so it moves with collector and page size; the boundary itself scales with74`ObjectAlignmentInBytes` — at 16, `-Xmx60g` is on and `-Xmx64g` off `[executed]`. Past it,75`UseCompressedOops` reads `false {default}`, not `{ergonomic}`, so read the value and not76the origin; `-Xlog:gc+init` prints `Compressed Oops: Enabled (32-bit)` or `Disabled`77(`references/production-footprint-checks.md` §2). Past that boundary recompute78`p` with79`ref` = 8; **the rule is unchanged but its answers move, including which classes save.** Five80of the fourteen rows in `compact-object-headers.md` §2 reverse, and `Object[]` becomes an818-byte-element array that shrinks by nothing at any length. That matters here because a82population large enough to ask this skill's question is often a heap large enough to cross the83threshold. _Where_ the threshold is as a heap-sizing decision is `jvm-performance-review`'s.8485Which header column is in force: classic on JDK 21, 25 and 26 `[executed]`; compact on JDK 2786`[source-only: JEP 534, Closed / Delivered, Release 27]` — JDK 27 is not GA and nothing in87this skill was run on it. The flag is experimental on 24 and needs88`-XX:+UnlockExperimentalVMOptions` there (JEP 450); a product flag on 25 (JEP 519, executed:89no unlock needed); default on 27 (JEP 534). A JDK 24 command line pasted onto 25 works; a 2590line pasted onto 24 does not. The rest of the flag's lifecycle and its cost belong to91`jvm-performance-review`.9293Three things the arithmetic gets wrong if you stop before the `alignUp`:9495- `record Point(int,int)` computes to 12 + 8 = 20 and **is 24**.96- `byte[1]` computes to 17 and **is 24**. So do `byte[2]` through `byte[8]`.97- **Declaration order is not the layout.** Fields are grouped by descending size with98 references placed last, and under classic headers a 4-byte field is hoisted into the99 12–15 header hole ahead of the 8-byte group. You cannot compute an offset from source100 order; you can compute a size.101102## Workflow1031041. **Pin the header mode before anything else**, on the target build — never assumed from the105 release number, and never read off the command line. The flag can read `true` where it was106 passed and `false` where it runs: two conditions cause that on 25.0.3 — disabled compressed107 class pointers, and a heap larger than 8191 GB with header-based forwarding — each108 announced only by a one-line `warning` on stderr that nobody reads.109110 ```bash111 java <same target flags> -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders112 jcmd <pid> VM.flags -all | grep UseCompactObjectHeaders # already running113 ```114115 `-all` is not optional: plain `VM.flags` prints nothing at all for a JVM sitting at the116 default, and the one character that carries the answer — the `+`/`-` — is the first thing a117 `grep -o` throws away. Read `references/compact-object-headers.md` §4 whenever118 `-XX:+UseCompactObjectHeaders` appears anywhere in the artefact — it has the three origin119 tags to match on. Why the JVM overrode it, and what that means for the rest of the120 configuration, is `jvm-performance-review`'s.1211222. **Compute the per-element size a priori** with the arithmetic above. For arrays, for the123 per-length table, and for the superclass gap-filling rule, read124 `references/array-and-object-arithmetic.md`. Do this before measuring: a prediction that125 the measurement then confirms is worth far more than a measurement alone, because it is126 the prediction that transfers to the next class.1273. **Decide whether compact object headers change the answer.** They are not a uniform128 8-byte saving and they are zero on several of the commonest classes in a Java heap. Read129 `references/compact-object-headers.md` for the measured per-class table and the rule that130 predicts each row. Never multiply 8 bytes by an object count.1314. **Only now compare shapes.** Record versus final class versus primitive array versus132 parallel arrays versus a boxed collection, at the stated population size, in measured133 bytes per element. Read `references/shape-decision.md`. Emit bytes per element and the134 total at N, in both header modes when supported; otherwise label the alternate model135 hypothetical rather than changing the project baseline. Never report a percentage alone.1365. **Measure to confirm the prediction.** Read `references/jol-operating-procedure.md` for137 the invocation that works on JDK 25/26, the four ways JOL fails — one of which throws on138 the first record you try — and the `Instrumentation.getObjectSize` cross-check. Read it139 before the first JOL run, not after the first stack trace. When the population already140 lives in a JVM you cannot attach JOL to, `jcmd <pid> GC.class_histogram` reports shallow141 sizes computed by that JVM in its own header mode — `Point` 24 → 16 `[executed]` — and142 `references/production-footprint-checks.md` §1 says what a heap dump cannot tell you.1436. **Report with the gate satisfied.** Build, tool, header mode, and shallow versus deep144 stated explicitly. If the number came from a source rather than a run, label it as145 source-derived; JDK 27 is not GA and nothing about it here was executed.146147## The headline: the record-versus-array intuition is backwards148149For a four-`long` payload, measured on Temurin 25.0.3+9 and 26.0.2+10, JOL 0.17 agreeing150with `Instrumentation.getObjectSize`:151152| Shape | Arithmetic (classic) | Classic | Arithmetic (compact) | Compact |153| --------------------- | --------------------- | ------- | --------------------- | ------- |154| `record Rec4(long×4)` | 12 hdr + 32 → align8 | **48** | 8 hdr + 32 → align8 | **40** |155| `long[4]` | 16 base + 32 → align8 | **48** | 16 base + 32 → align8 | **48** |156157Under classic headers they **tie**: the record's 4-byte header hole exactly cancels the158array's 4-byte length field. Under compact object headers the **record wins by 8 bytes**,159because 8-byte elements must stay 8-byte aligned, so `long[]` spends the freed header bytes160on a pad and shrinks by nothing at any length.161162"Drop the record for a primitive array to save the header" is therefore wrong for a163four-`long` payload today and **more** wrong once compact headers are the default. The164direction of the comparison changes, not just its magnitude — which is why a footprint165optimisation justified on JDK 25 must be re-measured before it ships on JDK 27.166167The intuition is only right when the array amortises **one header across many elements**.168Parallel primitive arrays beat a million records by 20 bytes each — 24.00 against 44.00 bytes169per element, measured (`shape-decision.md` §1). One `long[4]` beats a single `Rec4` by170nothing. That distinction — replacing N headers, not replacing one — is the break-even, and171it is the point of that reference.172173## Rules174175- **Treat compact object headers as a measured deployment decision.** Answer176 only the footprint half: the saving is workload-specific and frequently zero, so quote it177 from the class mix, and say what would prove it — the same live-set measurement in both178 modes on the same build (heap after a verified collection, or `GraphLayout.totalSize()` over the actual179 population), never an object count times eight. **What the flag costs, its prerequisites,180 its lifecycle and its per-release defaults are `jvm-performance-review`'s; route there181 rather than summarising.**182- **Distinguish the boxed type, collection structure and reference width.**183 Under compressed oops `Integer`, `Boolean`, `ArrayList` and the `String` object are all184 unchanged, and `ArrayList<Integer>` of 1000 distinct values measured **20,976 bytes in both185 modes** on 25.0.3 and 26.0.2. At 32 GB and above the same population measures 25,920 → 25,912 — the186 `ArrayList` itself now saves 8 bytes and nothing else does, so the conclusion survives but187 the equality does not. A boxed-collection-heavy heap is the case where an "8 bytes per188 object" plan overstates the saving. This is not a rule for all boxed collections:189 `Long`/`Double` boxes and, with compressed oops, map nodes do shrink in the tables.190- **State the encoding — and the oop size — before making any claim about a string.** Under191 compressed oops the `String` object is 24 bytes in both modes at every length, so only its192 `byte[]` payload can shrink and the rule runs over **payload bytes**: 8 when193 `(length × bytesPerChar) mod 8` ∈ {1,2,3,4}, else 0. With `COMPACT_STRINGS` on by default,194 `bytesPerChar` is 1 for a Latin-1-representable string and 2 for anything containing a195 character above U+00FF, and the two encodings give **opposite** answers at 3–6 characters:196 "5 to 8 characters gains nothing" is true for ASCII and false for UTF-16 at 5–6. At 32 GB and above197 the `String` object itself goes 32 → 24, so it saves 8 regardless of payload and the whole198 example inverts — `String[1000]` of 8-character strings goes from a flat 52,016 to199 64,016 → 56,016, a 12.5% saving `[executed]`. `compact-object-headers.md` §3 has every200 measured column.201- **Shallow is not deep, and the gap is the whole answer for anything holding references.**202 `ClassLayout.instanceSize()` on `new String("EUR")` is 24 bytes; `GraphLayout.totalSize()`203 is 48. Name which one you measured, every time.204- **Version-scope every size, and label anything not executed.** JDK 27 is not GA. Its205 default header mode is read from JEP 534 (`Closed / Delivered`, Release 27, confirmed at206 `openjdk.org/jeps/534`), not observed. `-XX:+UseCompactObjectHeaders` does not exist at all207 on JDK 21 — the JVM refuses to start with `Unrecognized VM option` (executed, 21.0.12+8).208- **Match sharing to the question.** To model distinct boxes, verify values are outside the209 configured `Integer` cache (which can exceed 127). To model a real cached population,210 retain its sharing: JOL counts each reachable box once, not once per reference. Reachable211 bytes are not incremental allocation or retained bytes. See the JOL procedure's array-root trap.212- **A per-object saving is not a heap saving until it is multiplied by the live population.**213 Eight bytes off `HashMap$Node` saves about 8 KB at a thousand entries and 320 MB at forty214 million. Compare with the required headroom; ask for N before estimating a total.215- **Do not reach for `-XX:+PrintFieldLayout`.** It is a `develop` flag: on the tested production216 JDK the JVM refuses to start with it (executed, 25.0.3), there is no `-Xlog` equivalent,217 and JOL is the tool used here on production builds. Direct VM/offset tooling is another218 option; consult the JOL reference for the source-only change on JDK 28 development builds.219- **Compact object headers buy heap bytes with class-space bytes.** The 22-bit class220 pointer used a shift of 10 in the measured configuration: **537 → 1,024 bytes per class** of221 compressed class space (executed, 25.0.3, 100,000 strong hidden classes), so the 1 GB222 default holds ~1.06 M classes instead of ~2 M. On a proxy- or lambda-heavy service read223 `jcmd <pid> VM.metaspace` before switching, and quote both sides.224 `references/production-footprint-checks.md` §3.225- **Under G1, an object whose aligned size exceeds half a region reserves whole regions.**226 `byte[600000]` requires one 1,048,576-byte region with 1 MB regions;227 `byte[1100000]` requires two. The historical aggregate heap deltas in the production228 reference are estimates, not exact object sizes or histogram charges.229 The per-object arithmetic is exact and the heap cost is still wrong by up to a region per230 array — size chunks against `G1HeapRegionSize`.231 `references/production-footprint-checks.md` §4.232- **Hashing and locking did not change shallow object size on the tested JDK 25 build**, in233 either header mode (executed):234 the identity hash and the monitor state live in the mark word or beside the object. Do235 not carry "hashed objects get bigger" from other or proposed header layouts into this build.236237## References238239- [Array and object arithmetic](references/array-and-object-arithmetic.md) — the header240 composition, the length field, element alignment, the per-length size table for241 `byte[]`/`int[]`/`long[]`/`Object[]` in both modes, the measured field-ordering and242 superclass gap-filling rules, and what `ObjectAlignmentInBytes=16` costs per object. Read243 at step 2, whenever a size is being computed rather than measured.244- [Compact object headers, measured](references/compact-object-headers.md) — which objects245 shrink and which do not, with the rule that predicts every row; the two conditions that246 disable the flag while it still reads as set; and the deep-footprint tables showing where247 the saving is zero. Read at step 1 whenever the flag appears, and at step 3 always.248- [The shape decision](references/shape-decision.md) — record versus final class versus249 primitive array versus parallel arrays versus `ArrayList` versus `HashMap`, measured at250 N = 1,000,000 in bytes per element under both header modes, with the break-even reasoning251 and the costs that are not bytes. Read at step 4.252- [JOL operating procedure](references/jol-operating-procedure.md) — the exact invocation253 for JDK 25/26, the four failure modes with their verbatim messages and fixes, and the254 two-file `Instrumentation.getObjectSize` agent that cross-checks JOL rather than trusting255 it. Read at step 5, before the first run.256- [Production footprint checks](references/production-footprint-checks.md) — sizes the257 running JVM reports without JOL (`GC.class_histogram`, `jdk.ObjectCount`) and why a heap258 dump is not one of them; the compressed-oops boundary by alignment and the origin-tag259 trap; the 1 KB-per-`Klass` class-space cost of compact headers; G1 humongous rounding260 for large arrays; what never changes an object's size; and the symptom-to-cause table for261 a prediction that disagrees with an observation. Read at step 5 when JOL cannot be262 attached, and at step 6 whenever the numbers disagree.263264Authoritative sources for release-sensitive claims:265266- [JEP 450: Compact Object Headers (Experimental)](https://openjdk.org/jeps/450)267- [JEP 519: Compact Object Headers](https://openjdk.org/jeps/519)268- [JEP 534: Compact Object Headers by Default](https://openjdk.org/jeps/534)269- [`Instrumentation.getObjectSize`](<https://docs.oracle.com/en/java/javase/25/docs/api/java.instrument/java/lang/instrument/Instrumentation.html#getObjectSize(java.lang.Object)>)270- [OpenJDK JOL](https://github.com/openjdk/jol) — verify the current release and tool limitations271- [Oracle JDK GC Tuning Guide: class metadata and compact headers](https://docs.oracle.com/en/java/javase/26/gctuning/other-considerations.html)