Unpacking Java 25’s Change on compacting object headers

If you’re a Java developer, you know that memory and performance are king. We’re always looking for ways to make our applications run faster and use fewer resources. Well, Java 25 has a fantastic new feature that does just that, and it’s called Compact Object Headers (JEP 519).

It might sound complicated, but the idea is simple and the impact can be huge. Let’s dive in!


What’s the Deal with Object Headers Anyway?

Every time you create an object in Java (new MyObject()), the JVM attaches a little piece of metadata to it called a header. Think of it as a small “ID card” for the object. This header tells the JVM important stuff, like:

Historically, on a typical 64-bit system, this header took up 12 bytes (96 bits). It was split into two parts: a 64-bit “mark word” and a 32-bit “class pointer”.

12 bytes might not sound like much. But what if you have an application with millions, or even billions, of tiny objects? That’s a huge difference!


The Big Squeeze: What Java 25 Changes

Java 25 makes a optimization: it shrinks the object header from 12 bytes down to just 8 bytes.

How? It cleverly merges the mark word and the class pointer into a single 64-bit field. This is a big deal because it’s now a standard, production-ready feature. While it was available in Java 24 (-XX:+UnlockExperimentalVMOptions), you had to enable experimental flags.

The immediate benefits are pretty awesome:

This change is especially powerful for applications with tons of small objects, which is common in modern microservices and data-processing workloads.


Let’s test it! A Simple Benchmark

Talk is cheap, right? Let’s write a simple test. We’ll create a basic Java application that allocates many small objects and then use a monitoring tool to check what’s going on.

The Tool: JConsole

The easiest way to see what’s happening inside your JVM is with jconsole. It’s a tool that comes with JDK by default, so you should be able to use it right a way. It lets you monitor memory usage, CPU, threads, and GC activity in real-time.

The Code

Here’s our simple benchmark application. It contains a loop that allocates small objects in 30k portions until run duration is hited.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class BenchmarkApp {

    static class SmallObject {
        private int data;

        public SmallObject(int data) {
            this.data = data;
        }
    }

    private static final long RUN_DURATION_MS = TimeUnit.MINUTES.toMillis(2);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("--- GC Benchmark Application ---");
        System.out.println("Process ID: " + ProcessHandle.current().pid());
        System.out.println("Initial pause (20s) to connect profiler :)");

        Thread.sleep(20000);

        System.out.println("We're starting the loop of allocation.");
        System.out.println("Observe the usage of heap, CPU and GC spent time!");

        long startTime = System.currentTimeMillis();

        while (System.currentTimeMillis() - startTime < RUN_DURATION_MS) {

            List<SmallObject> transientObjects = new ArrayList<>();
            int numObjects = 30_000;

            for (int i = 0; i < numObjects; i++) {
                transientObjects.add(new SmallObject(i));
            }

            Thread.sleep(1);
        }
        System.out.println("The loop ended. App is exiting...");
    }
}


Running the Test

First, let’s compile the code:

javac BenchmarkApp.java

Now, we will run the test twice. We’ll use -Xms128m -Xmx128m to give the JVM a fixed heap size of 128mb for a fair comparison.

Run 1: Standard Java 25 (Without Compact Headers)

  1. Run the application with this command:
    java -Xms128m -Xmx128m BenchmarkApp
    
  2. Open Jconsole and connect to the BenchmarkApp process.

Run 2: Java 25 with Compact Headers Enabled

  1. Now, run it again, but this time with the magic flag: -XX:+UseCompactObjectHeaders.
    java -XX:+UseCompactObjectHeaders -Xms128m -Xmx128m BenchmarkApp
    
  2. Again, open Jconsole, connect to the new process.

Compare your results!

Analyzing My Results

I picked one of the few runs summary I launched, all of them were very similar to each other.

ScenarioYoung Gen Collections (count)Time taken
Old Style (Compaction off)1592≈1.291 seconds
New Compaction1374≈1.055 seconds

The verdict Honestly? The difference was pretty minor.


Conclusion: a small change, worth trying

Testing on a personal machine just doesn’t give the full picture of how things work in the real world. The new compaction is definitely a good thing, but it’s most likely to be a game-changer for:

In those environments, the reduced memory footprint and improved CPU cache efficiency from compaction should actually make a noticeable difference!

So go ahead, give this flag a try on your applications and see the performance gains for yourself, happy coding!

© 2025 dominik_dev

𝕏 GitHub Linkedin