skip to content

Why can raising a HotSpot maximum heap setting from 31 GB to 33 GB leave a Java service with less usable capacity than it had before?

level: middleimportance: should knowfreq 35%

answer

  1. 32-bit refs scaled by 8-byte alignment ~ 32 GB
  2. UseCompressedOops silently off above the threshold
  3. references double: live set grows 10-20%, more if pointer-dense
  4. stay near 30-31 GB or jump well past 40 GB
  5. ObjectAlignmentInBytes=16 raises the ceiling, wastes padding

basics

~20 s

Below roughly 32 GB, HotSpot stores object references as 32-bit compressed pointers. Above that threshold it must use full 64-bit references, so every object with references grows and the whole live set expands - typically enough to swallow the extra 2 GB and more.

solid answer

~50 s

HotSpot uses compressed ordinary object pointers: object references are stored as 32-bit values scaled by the 8-byte object alignment, which addresses about 32 GB of heap. -XX:+UseCompressedOops is enabled automatically while the maximum heap stays below that threshold, and it is silently disabled above it. Once disabled, every reference field, every array element of reference type and every object header pointer doubles from four to eight bytes. Typical object graphs grow by 10-20%, and pointer-dense structures such as linked lists, trees and maps grow more. Compressed class pointers are affected as well, so class metadata grows too. The effective live set can therefore expand by more than the 2 GB you added, and cache efficiency worsens because fewer objects fit in a cache line. The practical rules: keep the maximum heap comfortably under the threshold, around 30-31 GB, or jump far enough past it (well beyond 40 GB) that the extra capacity outweighs the expansion. -XX:ObjectAlignmentInBytes=16 raises the ceiling at the cost of alignment waste.

code

text · 5 lines
text
$ java -Xmx31g -XX:+PrintFlagsFinal -version | grep UseCompressedOops
     bool UseCompressedOops = true

$ java -Xmx33g -XX:+PrintFlagsFinal -version | grep UseCompressedOops
     bool UseCompressedOops = false     # silently disabled

go deeper

for a junior

Know that below roughly 32 GB the JVM stores references in four bytes instead of eight, and that crossing that line makes objects bigger.

for a middle

Explain the scaling by object alignment, the automatic enable/disable, and the resulting live-set expansion.

for a senior

Advise on heap sizing around the cliff, verify the flag on the deployed JDK, and weigh multiple JVMs or off-heap storage against one large heap.

for a principal

Treat it as an architectural constraint on single-process data size, driving decisions about sharding, off-heap storage and instance shape rather than a flag to toggle.

## What compressed references are On a 64-bit JVM a native pointer is eight bytes. Since references are the most common field type in a typical object graph, storing all of them at full width is expensive in both footprint and cache behaviour. HotSpot's compressed ordinary object pointers store a reference as a 32-bit value that is not a raw address but an index of 8-byte-aligned slots, optionally offset from a heap base. Because objects are aligned to 8 bytes by default, a 32-bit value can index 2^32 slots of 8 bytes each - about 32 GB of heap. Decoding is a shift and, if the heap does not start at a convenient address, an add; when the JVM can place the heap so that no base offset is needed, decoding is a shift alone or even free. The feature is automatic. -XX:+UseCompressedOops is on by default when the configured maximum heap is small enough for the encoding to address it, and the JVM turns it off when it is not. There is no warning in normal output, which is exactly why the cliff surprises people. ## The cliff Set the maximum heap just under the threshold and every reference costs four bytes. Set it just over and every reference costs eight. The change applies to instance fields of reference type, elements of object arrays, and the class-pointer word in each object header (compressed class pointers ride on the same mechanism). Object headers themselves and alignment padding shift accordingly. For a typical business object graph this expands the live set by roughly 10-20%. For pointer-dense structures - linked lists, trees, hash-map node chains, graphs of small objects - the expansion is larger, because references are a bigger fraction of each object. If your live set was 20 GB in a 31 GB heap, it may become 23-24 GB in a 33 GB heap: you added 2 GB of capacity and consumed more than that in overhead, ending with less usable headroom than before. Collection work rises in step, since the collector copies and marks more bytes for the same logical data. There is a second-order cost too. Doubling reference width means fewer objects and fewer references per cache line, so pointer-chasing workloads lose throughput independently of GC. ## What to do instead Stay below the threshold. The practical advice is to keep the maximum heap around 30-31 GB rather than pushing right up to it, because the exact cut-off depends on the JDK version, whether a zero-based heap can be placed, and other reservations. Verify rather than assume: printing the final flag value shows whether the JVM actually enabled the feature for your configuration. If you genuinely need more memory than that, jump decisively. Going to 48, 64 GB or beyond means the added capacity comfortably exceeds the expansion, so uncompressed references are simply the cost of doing business. What you should not do is land in the dead zone between roughly 32 and 40 GB, where you pay the full expansion for little extra usable space. A third option is -XX:ObjectAlignmentInBytes=16, which doubles the addressable range of the encoding to about 64 GB. It is rarely a good trade: every object is padded to a 16-byte boundary, wasting space on small objects, and it interacts with collector assumptions. Treat it as a specialist option, not a default. The fourth and often best answer for large data is architectural: run several JVMs with heaps under the threshold instead of one huge one, or move the bulk data off-heap or into a purpose-built store, so the collected heap stays in the range where references are cheap. ## Version note The mechanism and the roughly 32 GB figure are HotSpot behaviour and stable across modern JDKs, but the exact cut-off and the heap-base placement heuristics vary by version and by other reservations in the address space, so measure on the JDK you actually deploy.

  • Which kinds of application data suffer most when compressed references are disabled?
    Pointer-dense structures: linked lists, trees, graphs, hash-map node chains and collections of small objects, where references make up a large share of each object's size. Data dominated by primitive arrays or long strings is affected far less, because the bulk of those bytes are payload rather than references.
  • You need a 40 GB live set. What are your realistic options?
    Either accept uncompressed references and size the heap well above 40 GB so the added capacity outweighs the roughly 10-20% expansion, or avoid the cliff architecturally - split into multiple JVMs each with a heap under the threshold, or move bulk data off-heap or into an external store so the collected heap stays in the compressed range.

Like moving to a bigger warehouse that only accepts full-size pallets: you gained floor space, but every item now takes twice the footprint, so you store less than you did before.

saying these in an interview costs you the question

  • Assuming heap capacity scales linearly through the 32 GB boundary
  • Thinking compressed references must be enabled manually rather than being an automatic default
  • Believing the threshold comes from a 4 GB addressing limit rather than 32-bit references scaled by 8-byte alignment
  • Treating -XX:ObjectAlignmentInBytes=16 as a free way to keep compressed references
  • Not verifying the flag's final value on the deployed JDK before choosing a heap size

context